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'; import config from 'croodle/config/environment';
export default class App extends Application { export default class App extends Application {
LOG_TRANSITIONS = true;
modulePrefix = config.modulePrefix; modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix; podModulePrefix = config.podModulePrefix;
Resolver = Resolver; 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|}} {{#let @formElement as |FormElement|}}
<form.element <FormElement
@label={{t "create.options.dates.label"}} @label={{t "create.options.dates.label"}}
@property="options" @property="options"
data-test-form-element-for="days" data-test-form-element-for="days"
@ -17,10 +17,7 @@
<InlineDatepicker <InlineDatepicker
@center={{this.calendarCenter}} @center={{this.calendarCenter}}
@selectedDays={{this.selectedDays}} @selectedDays={{this.selectedDays}}
@onCenterChange={{action @onCenterChange={{fn this.handleCalenderCenterChange 0}}
(mut this.calendarCenter)
value="datetime"
}}
@onSelect={{this.handleSelectedDaysChange}} @onSelect={{this.handleSelectedDaysChange}}
/> />
</div> </div>
@ -28,14 +25,11 @@
<InlineDatepicker <InlineDatepicker
@center={{this.calendarCenterNext}} @center={{this.calendarCenterNext}}
@selectedDays={{this.selectedDays}} @selectedDays={{this.selectedDays}}
@onCenterChange={{action @onCenterChange={{fn this.handleCalenderCenterChange -1}}
(mut this.calendarCenter)
value="datetime"
}}
@onSelect={{this.handleSelectedDaysChange}} @onSelect={{this.handleSelectedDaysChange}}
/> />
</div> </div>
</div> </div>
</div> </div>
</form.element> </FormElement>
{{/let}} {{/let}}

View file

@ -3,12 +3,13 @@ import { action } from '@ember/object';
import { isArray } from '@ember/array'; import { isArray } from '@ember/array';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import type { TrackedSet } from 'tracked-built-ins';
import type { FormDataOption } from './create-options'; import type { FormDataOption } from './create-options';
import type BsFormElementComponent from 'ember-bootstrap/components/bs-form/element';
export interface CreateOptionsDatesSignature { export interface CreateOptionsDatesSignature {
Args: { Args: {
options: TrackedSet<FormDataOption>; formElement: BsFormElementComponent;
options: Array<FormDataOption>;
updateOptions: (options: string[]) => void; updateOptions: (options: string[]) => void;
}; };
} }
@ -20,7 +21,7 @@ export default class CreateOptionsDates extends Component<CreateOptionsDatesSign
: DateTime.local(); : DateTime.local();
get selectedDays(): DateTime[] { get selectedDays(): DateTime[] {
return Array.from(this.args.options).map( return this.args.options.map(
({ value }) => DateTime.fromISO(value) as DateTime, ({ value }) => DateTime.fromISO(value) as DateTime,
); );
} }
@ -45,4 +46,18 @@ export default class CreateOptionsDates extends Component<CreateOptionsDatesSign
newDatesAsLuxonDateTime.map((datetime) => datetime.toISODate() as string), 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"> <div class="cr-form-wrapper box">
{{#if this.errorMessage}} {{#if this.errorMessage}}
<BsAlert type="warning"> <BsAlert @type="warning">
{{t this.errorMessage}} {{t this.errorMessage}}
</BsAlert> </BsAlert>
{{/if}} {{/if}}
@ -15,33 +15,15 @@
> >
<div class="days"> <div class="days">
{{#each-in this.formData.datetimes as |date timeOptions|}} {{#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|}} {{#each timeOptions as |timeOption indexInTimeOptions|}}
{{! <div data-test-day={{date}}>
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}}
>
<form.element <form.element
{{! @label={{format-date timeOption.jsDate dateStyle="full"}}
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6
}}
@label={{format-date
timeOption.jsDate
weekday="long"
day="numeric"
month="long"
year="numeric"
}}
{{! {{!
show label only for the first time of this date show label only for the first time of this date
}} }}

View file

@ -102,6 +102,19 @@ class FormData {
return this.datetimes.size > 1; 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 @action
addOption(date: string) { addOption(date: string) {
this.datetimes this.datetimes
@ -215,7 +228,7 @@ export default class CreateOptionsDatetime extends Component<CreateOptoinsDateti
// validate input field for being partially filled // validate input field for being partially filled
@action @action
validateInput(option: FormDataTimeOption, event: InputEvent) { validateInput(option: FormDataTimeOption, event: Event) {
const element = event.target as HTMLInputElement; const element = event.target as HTMLInputElement;
// update partially filled time validation error // 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 // remove partially filled validation error if user fixed it
@action @action
updateInputValidation(option: FormDataTimeOption, event: InputEvent) { updateInputValidation(option: FormDataTimeOption, event: Event) {
const element = event.target as HTMLInputElement; const element = event.target as HTMLInputElement;
if (element.checkValidity() && option.isPartiallyFilled) { if (element.checkValidity() && option.isPartiallyFilled) {
@ -266,3 +279,9 @@ export default class CreateOptionsDatetime extends Component<CreateOptoinsDateti
this.router.on('routeWillChange', this.handleTransition); 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|}} {{#each @options as |option index|}}
<form.element <FormElement
{{! show label only on first item }} {{! show label only on first item }}
@label={{unless index (t "create.options.options.label")}} @label={{unless index (t "create.options.options.label")}}
@model={{option}} @model={{option}}
@ -48,6 +48,6 @@
></span> ></span>
<span class="sr-only">{{t "create.options.button.add.label"}}</span> <span class="sr-only">{{t "create.options.button.add.label"}}</span>
</BsButton> </BsButton>
</form.element> </FormElement>
{{/each}} {{/each}}
{{/let}} {{/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}} @options={{this.formData.options}}
@addOption={{this.formData.addOption}} @addOption={{this.formData.addOption}}
@deleteOption={{this.formData.deleteOption}} @deleteOption={{this.formData.deleteOption}}
@form={{form}} @formElement={{form.element}}
/> />
{{else}} {{else}}
<CreateOptionsDates <CreateOptionsDates
@options={{this.formData.options}} @options={{this.formData.options}}
@updateOptions={{this.formData.updateOptions}} @updateOptions={{this.formData.updateOptions}}
@form={{form}} @formElement={{form.element}}
/> />
{{/if}} {{/if}}
@ -27,7 +27,7 @@
<NextButton /> <NextButton />
</div> </div>
<div class="col-6 col-md-4 order-1 text-right"> <div class="col-6 col-md-4 order-1 text-right">
<BackButton @onClick={{action "previousPage"}} /> <BackButton @onClick={{this.previousPage}} />
</div> </div>
</div> </div>
</BsForm> </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; @service declare router: RouterService;
formData = new FormData( formData = new FormData(
@ -136,3 +136,9 @@ export default class CreateOptionsComponent extends Component<CreateOptionsSigna
this.router.on('routeWillChange', this.handleTransition); 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 <button
type="button" type="button"
class="ember-power-calendar-nav-control" class="ember-power-calendar-nav-control"
onclick={{action calendar.actions.moveCenter -1 "month"}} {{on "click" (fn calendar.actions.moveCenter -1 "month")}}
> >
« «
</button> </button>
@ -19,7 +19,7 @@
<button <button
type="button" type="button"
class="ember-power-calendar-nav-control" class="ember-power-calendar-nav-control"
onclick={{action calendar.actions.moveCenter 1 "month"}} {{on "click" (fn calendar.actions.moveCenter 1 "month")}}
> >
» »
</button> </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 <BsButton
@buttonType="submit"
@type="primary" @type="primary"
class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next" class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next"
type="submit"
...attributes ...attributes
> >
<span class="cr-steps-bottom-nav__label"> <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 }} {{! column for name }}
</th> </th>
{{#each-in this.optionsPerDay as |jsDate count|}} {{#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}}> <th colspan={{count}}>
{{! {{!
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6 TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6
@ -37,21 +43,23 @@
{{#if (and @poll.isFindADate @poll.hasTimes)}} {{#if (and @poll.isFindADate @poll.hasTimes)}}
{{#if option.hasTime}} {{#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}} {{/if}}
{{else if @poll.isFindADate}} {{else if @poll.isFindADate}}
{{! {{!
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6 @glint-ignore
}} Narrowring is not working here correctly. Due to the only executing if
{{format-date `option.hasTime` is `true`, we know that `option.jsDate` cannot be `null`.
option.jsDate But TypeScript does not support narrowing through a chain of getters
weekday="long" currently.
day="numeric"
month="long"
year="numeric"
}} }}
{{format-date option.jsDate dateStyle="full"}}
{{else}} {{else}}
{{option.title}} {{option.title}}
{{/if}} {{/if}}
@ -67,7 +75,7 @@
{{user.name}} {{user.name}}
</td> </td>
{{#each @poll.options as |option index|}} {{#each @poll.options as |option index|}}
{{#let (object-at index user.selections) as |selection|}} {{#let (get user.selections index) as |selection|}}
<td <td
class={{selection.type}} class={{selection.type}}
data-test-is-selection-cell data-test-is-selection-cell

View file

@ -12,11 +12,19 @@ export default class PollEvaluationParticipantsTable extends Component<PollEvalu
get optionsPerDay() { get optionsPerDay() {
const { poll } = this.args; const { poll } = this.args;
const optionsPerDay = new Map(); const optionsPerDay: Map<string, number> = new Map();
for (const option of poll.options) { 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( optionsPerDay.set(
option.day, 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 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. 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 }} Checking `@evaluationBestOption.option.jsDate` is the same as checking `@isFindADate`.
{{! template-lint-disable block-indentation }} 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}}> <strong data-test-best-option={{@evaluationBestOption.option.title}}>
{{!
TODO: Simplify to dateStyle="full" and timeStyle="short" after upgrading to Ember Intl v6
}}
{{format-date {{format-date
@evaluationBestOption.option.jsDate @evaluationBestOption.option.jsDate
weekday="long" dateStyle="full"
day="numeric" timeStyle=(if @evaluationBestOption.option.hasTime "short" undefined)
month="long"
year="numeric"
hour=(if @evaluationBestOption.option.hasTime "numeric" undefined)
minute=(if @evaluationBestOption.option.hasTime "numeric" undefined)
timeZone=(if @timeZone @timeZone undefined) timeZone=(if @timeZone @timeZone undefined)
}}</strong>. }}</strong>.
{{! template-lint-enable block-indentation }}
{{else}} {{else}}
<strong <strong
data-test-best-option={{@evaluationBestOption.option.title}} 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}}
{{#if (get this.bestOptions.length 1)}} {{#if (gt this.bestOptions.length 1)}}
<ul> <ul>
{{#each this.bestOptions as |evaluationBestOption|}} {{#each this.bestOptions as |evaluationBestOption|}}
<li> <li>
@ -34,6 +34,11 @@
</ul> </ul>
{{else}} {{else}}
<PollEvaluationSummaryOption <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}} @evaluationBestOption={{get this.bestOptions 0}}
@isFindADate={{@poll.isFindADate}} @isFindADate={{@poll.isFindADate}}
@timeZone={{@timeZone}} @timeZone={{@timeZone}}
@ -42,9 +47,16 @@
</p> </p>
<p class="last-participation"> <p class="last-participation">
{{t {{#if this.lastParticipationAt}}
"poll.evaluation.lastParticipation" {{t
ago=(format-date-relative this.lastParticipationAt) "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> </p>
</div> </div>

View file

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

View file

@ -1,7 +1,7 @@
<BsButton <BsButton
@buttonType="submit"
@type="primary" @type="primary"
class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next" class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next"
type="submit"
...attributes ...attributes
> >
<span class="cr-steps-bottom-nav__label"> <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.shouldUseLocalTimezone = true;
this.timezoneChoosen = true; this.timezoneChoosen = true;
} }
@action
usePollTimezone() {
this.timezoneChoosen = true;
}
} }

View file

@ -9,19 +9,27 @@ export interface FormatDateRelativeHelperSignature {
Args: { Args: {
Positional: Positional; Positional: Positional;
}; };
Return: string;
} }
export default class FormatDateRelativeHelper extends Helper { export default class FormatDateRelative extends Helper<FormatDateRelativeHelperSignature> {
@service declare intl: IntlService; @service declare intl: IntlService;
compute([date]: Positional) { compute([dateOrIsoString]: Positional) {
if (date instanceof Date) { const isoString =
date = date.toISOString(); dateOrIsoString instanceof Date
} ? dateOrIsoString.toISOString()
: dateOrIsoString;
return DateTime.fromISO(date).toRelative({ return DateTime.fromISO(isoString).toRelative({
locale: this.intl.primaryLocale, locale: this.intl.primaryLocale,
padding: 1000, 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([ const markAsSafeHtml = helper<MarkAsSafeHtmlHelperSignature>(([html]) => {
html,
]) {
return htmlSafe(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,47 +32,53 @@ function elementIsNotVisible(element: Element) {
); );
} }
export function scrollFirstInvalidElementIntoViewPort() { const scrollFirstInvalidElementIntoViewPort = helper(() => {
// `schedule('afterRender', function() {})` would be more approperiate but there seems to be a return () => {
// timing issue in Firefox causing the Browser not scrolling up far enough if doing so // `schedule('afterRender', function() {})` would be more approperiate but there seems to be a
// delaying to next runloop therefore // timing issue in Firefox causing the Browser not scrolling up far enough if doing so
next(function () { // delaying to next runloop therefore
const invalidInput = document.querySelector( next(function () {
'.form-control.is-invalid, .custom-control-input.is-invalid', const invalidInput = document.querySelector(
) as HTMLInputElement; '.form-control.is-invalid, .custom-control-input.is-invalid',
assert( ) as HTMLInputElement;
'Atleast one form control must be marked as invalid if form submission was rejected as invalid', assert(
invalidInput, 'Atleast one form control must be marked as invalid if form submission was rejected as invalid',
); invalidInput,
);
// focus first invalid control // focus first invalid control
invalidInput.focus({ preventScroll: true }); invalidInput.focus({ preventScroll: true });
// scroll to label or legend of first invalid control if it's not visible yet // scroll to label or legend of first invalid control if it's not visible yet
if (elementIsNotVisible(invalidInput)) { if (elementIsNotVisible(invalidInput)) {
// Radio groups have a label and a legend. While the label is per input, the legend is for // Radio groups have a label and a legend. While the label is per input, the legend is for
// the whole group. Croodle should bring the legend into view in that case. // the whole group. Croodle should bring the legend into view in that case.
// Due to a bug in Ember Bootstrap it renders a `<label>` instead of a `<legend>`: // Due to a bug in Ember Bootstrap it renders a `<label>` instead of a `<legend>`:
// https://github.com/kaliber5/ember-bootstrap/issues/931 // 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 // 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 `<label>` (which should be a `<legend>`). // inputs and the `for` of the input group `<label>` (which should be a `<legend>`).
const scrollTarget = const scrollTarget =
document.querySelector( document.querySelector(
`label[for="${invalidInput.id.substr( `label[for="${invalidInput.id.substr(
0, 0,
invalidInput.id.indexOf('_'), invalidInput.id.indexOf('_'),
)}"`, )}"`,
) || ) ||
document.querySelector(`label[for="${invalidInput.id}"]`) || document.querySelector(`label[for="${invalidInput.id}"]`) ||
// For polls with type `MakeAPoll` the option inputs do not have a label at all. In that case // For polls with type `MakeAPoll` the option inputs do not have a label at all. In that case
// we scroll to the input element itself // we scroll to the input element itself
invalidInput; invalidInput;
scrollTarget.scrollIntoView({ behavior: 'smooth' }); 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() { get day() {
if (!this.datetime) { if (this.datetime === null) {
return null; return null;
} }
@ -35,7 +35,7 @@ export default class Option {
} }
get jsDate() { get jsDate() {
if (!this.datetime) { if (this.datetime === null) {
return null; return null;
} }

View file

@ -1,11 +1,11 @@
import Modifier from 'ember-modifier'; import Modifier from 'ember-modifier';
type Named = { type Named = {
enabled: boolean; enabled?: boolean;
}; };
interface AutofocusModifierSignature { interface AutofocusModifierSignature {
Element: HTMLInputElement; Element: HTMLInputElement | HTMLSelectElement;
Args: { Args: {
Named: Named; Named: Named;
}; };
@ -29,3 +29,9 @@ export default class AutofocusModifier extends Modifier<AutofocusModifierSignatu
element.focus(); 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'> <nav class="cr-navbar navbar navbar-dark">
<h1 class='cr-logo'> <h1 class="cr-logo">
<LinkTo @route='index' class='navbar-brand'> <LinkTo @route="index" class="navbar-brand">
Croodle Croodle
</LinkTo> </LinkTo>
</h1> </h1>
<div class='collapse' id='headerNavbar'> <div class="collapse" id="headerNavbar">
<form class='form-inline my-2 my-lg-0'> <form class="form-inline my-2 my-lg-0">
<LanguageSelect class='custom-select custom-select-sm' /> <LanguageSelect />
</form> </form>
</div> </div>
</nav> </nav>
<main class='container cr-main'> <main class="container cr-main">
<div id='messages'> <div id="messages">
{{#each this.flashMessages.queue as |flash|}} {{#each this.flashMessages.queue as |flash|}}
<FlashMessage @flash={{flash}}> <FlashMessage @flash={{flash}}>
{{t flash.message}} {{t flash.message}}

View file

@ -1,23 +1,6 @@
{{page-title (t "create.title")}} {{page-title (t "create.title")}}
<BsButtonGroup @justified={{true}} class="cr-steps-top-nav form-steps"> <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 <BsButton
@onClick={{fn this.transitionTo "create.index"}} @onClick={{fn this.transitionTo "create.index"}}
@type={{if @type={{if

View file

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

View file

@ -5,4 +5,4 @@
<p> <p>
{{t "error.generic.unexpected.description"}} {{t "error.generic.unexpected.description"}}
</p> </p>
</div> </div>

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,8 +34,14 @@
<div class="col-lg-5 offset-lg-1"> <div class="col-lg-5 offset-lg-1">
<h3>{{t "index.hoster.title"}}</h3> <h3>{{t "index.hoster.title"}}</h3>
<p> <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> </p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -29,4 +29,4 @@
{{t "error.generic.unexpected.description"}} {{t "error.generic.unexpected.description"}}
</p> </p>
{{/if}} {{/if}}
</div> </div>

View file

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

View file

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

View file

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

View file

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

132
package-lock.json generated
View file

@ -15,8 +15,9 @@
"@ember/test-helpers": "^3.2.0", "@ember/test-helpers": "^3.2.0",
"@glimmer/component": "^1.1.2", "@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2", "@glimmer/tracking": "^1.1.2",
"@glint/environment-ember-loose": "^1.1.0", "@glint/core": "^1.2.1",
"@glint/template": "^1.1.0", "@glint/environment-ember-loose": "^1.2.1",
"@glint/template": "^1.2.1",
"@release-it-plugins/lerna-changelog": "^6.0.0", "@release-it-plugins/lerna-changelog": "^6.0.0",
"@tsconfig/ember": "^3.0.1", "@tsconfig/ember": "^3.0.1",
"@types/luxon": "^3.3.3", "@types/luxon": "^3.3.3",
@ -8913,6 +8914,84 @@
"@simple-dom/interface": "^1.4.0" "@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": { "node_modules/@glint/environment-ember-loose": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@glint/environment-ember-loose/-/environment-ember-loose-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@glint/environment-ember-loose/-/environment-ember-loose-1.2.1.tgz",
@ -56210,6 +56289,55 @@
"node": ">= 0.8" "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": { "node_modules/walk-sync": {
"version": "0.3.4", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-0.3.4.tgz", "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:hbs:fix": "ember-template-lint . --fix",
"lint:js": "eslint . --cache", "lint:js": "eslint . --cache",
"lint:js:fix": "eslint . --fix", "lint:js:fix": "eslint . --fix",
"lint:types": "tsc --noEmit", "lint:types": "glint",
"release": "release-it", "release": "release-it",
"start": "ember serve", "start": "ember serve",
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"", "test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
@ -34,8 +34,9 @@
"@ember/test-helpers": "^3.2.0", "@ember/test-helpers": "^3.2.0",
"@glimmer/component": "^1.1.2", "@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2", "@glimmer/tracking": "^1.1.2",
"@glint/environment-ember-loose": "^1.1.0", "@glint/core": "^1.2.1",
"@glint/template": "^1.1.0", "@glint/environment-ember-loose": "^1.2.1",
"@glint/template": "^1.2.1",
"@release-it-plugins/lerna-changelog": "^6.0.0", "@release-it-plugins/lerna-changelog": "^6.0.0",
"@tsconfig/ember": "^3.0.1", "@tsconfig/ember": "^3.0.1",
"@types/luxon": "^3.3.3", "@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 { setupMirage } from 'ember-cli-mirage/test-support';
import pageParticipation from 'croodle/tests/pages/poll/participation'; import pageParticipation from 'croodle/tests/pages/poll/participation';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { triggerCopySuccess } from 'ember-cli-clipboard/test-support';
module('Acceptance | view poll', function (hooks) { module('Acceptance | view poll', function (hooks) {
hooks.beforeEach(function () { hooks.beforeEach(function () {
window.localStorage.setItem('locale', 'en'); window.localStorage.setItem('locale', 'en');
@ -32,7 +30,7 @@ module('Acceptance | view poll', function (hooks) {
'share link is shown', 'share link is shown',
); );
await triggerCopySuccess(); await click('.copy-btn');
/* /*
* Can't test if link is actually copied to clipboard due to api * 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. * restrictions. Due to security it's not allowed to read from clipboard.

View file

@ -13,5 +13,8 @@
], ],
"*": ["types/*"] "*": ["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; exitTimer: number;
isExitable: boolean; isExitable: boolean;
initializedTime: number; initializedTime: number;
message: string;
destroyMessage(): void; destroyMessage(): void;
exitMessage(): void; exitMessage(): void;
preventExit(): 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 '@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 {}
}