UI improvments
This commit is contained in:
1 parent
c9dbe82b79
commit
0ce33cc9d6
106 files changed
+72934
-1675
No files matched your search
@@ -9,7 +9,7 @@
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="priority-statement">
|
||||
<mat-icon color="primary">info</mat-icon>
|
||||
<mat-icon aria-hidden="true">info_outline</mat-icon>
|
||||
<span>{{ tr().rules.priorityInfo }}</span>
|
||||
</div>
|
||||
|
||||
@@ -18,15 +18,22 @@
|
||||
<div
|
||||
class="rule-item"
|
||||
cdkDrag
|
||||
cdkDragLockAxis="y"
|
||||
[cdkDragStartDelay]="{ touch: 180, mouse: 0 }"
|
||||
[cdkDragDisabled]="editingRuleId() === rule.id"
|
||||
[class.editing]="editingRuleId() === rule.id"
|
||||
>
|
||||
@if (editingRuleId() !== rule.id) {
|
||||
<div class="rule-drag-grip">
|
||||
<mat-icon class="drag-handle-icon">drag_handle</mat-icon>
|
||||
</div>
|
||||
<div class="rule-display">
|
||||
<div class="rule-header">
|
||||
<div
|
||||
class="rule-drag-grip"
|
||||
cdkDragHandle
|
||||
[title]="tr().rules.dragRule"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<mat-icon>drag_indicator</mat-icon>
|
||||
</div>
|
||||
<mat-slide-toggle
|
||||
[checked]="rule.enabled"
|
||||
(change)="toggleRule(rule.id)"
|
||||
@@ -37,41 +44,53 @@
|
||||
{{ rule.name }}</span
|
||||
>
|
||||
</mat-slide-toggle>
|
||||
<div class="rule-actions">
|
||||
<button
|
||||
mat-icon-button
|
||||
[disabled]="i === 0"
|
||||
[aria-label]="tr().rules.moveRuleUp"
|
||||
(click)="moveRule(i, -1, $event)"
|
||||
>
|
||||
<mat-icon>keyboard_arrow_up</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
[disabled]="i === rules.length - 1"
|
||||
[aria-label]="tr().rules.moveRuleDown"
|
||||
(click)="moveRule(i, 1, $event)"
|
||||
>
|
||||
<mat-icon>keyboard_arrow_down</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
[aria-label]="tr().rules.editRule"
|
||||
(click)="startEdit(rule, $event)"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
[aria-label]="tr().rules.deleteRule"
|
||||
(click)="deleteRule(rule.id, $event)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-summary" [innerHTML]="getRuleSummary(rule)"></div>
|
||||
<div class="rule-summary">
|
||||
<span
|
||||
class="rule-force"
|
||||
[class.obligation]="rule.force === 'obligation'"
|
||||
>
|
||||
{{
|
||||
rule.force === 'obligation'
|
||||
? tr().rules.obligation
|
||||
: tr().rules.preference
|
||||
}}
|
||||
</span>
|
||||
<span [innerHTML]="getRuleSummary(rule)"></span>
|
||||
</div>
|
||||
<div class="rule-actions">
|
||||
<button
|
||||
mat-icon-button
|
||||
[disabled]="i === 0"
|
||||
[aria-label]="tr().rules.moveRuleUp"
|
||||
(click)="moveRule(i, -1, $event)"
|
||||
>
|
||||
<mat-icon aria-hidden="true">keyboard_arrow_up</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
[disabled]="i === rules.length - 1"
|
||||
[aria-label]="tr().rules.moveRuleDown"
|
||||
(click)="moveRule(i, 1, $event)"
|
||||
>
|
||||
<mat-icon aria-hidden="true">keyboard_arrow_down</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
[aria-label]="tr().rules.editRule"
|
||||
(click)="startEdit(rule, $event)"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="warn"
|
||||
[aria-label]="tr().rules.deleteRule"
|
||||
(click)="deleteRule(rule.id, $event)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="rule-editor">
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
.rules-panel-container,
|
||||
:host {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rules-panel-container {
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--ui-border);
|
||||
background: var(--ui-card-bg);
|
||||
border-radius: var(--ui-radius-md, 0.75rem);
|
||||
overflow: hidden;
|
||||
container-type: inline-size;
|
||||
|
||||
mat-expansion-panel-header {
|
||||
padding-inline: 0.75rem;
|
||||
min-height: 3.5rem;
|
||||
height: auto;
|
||||
padding-block: 0.625rem;
|
||||
}
|
||||
mat-panel-title {
|
||||
margin: 0 0.5rem 0 0;
|
||||
}
|
||||
|
||||
mat-panel-title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -18,6 +40,7 @@
|
||||
border-radius: 999px;
|
||||
color: var(--mat-sys-on-primary-container, #001b3f);
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -37,20 +60,16 @@
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
container: rule-item / inline-size;
|
||||
font: var(--mat-sys-body-medium);
|
||||
letter-spacing: var(--mat-sys-body-medium-tracking);
|
||||
border: 1px solid var(--mat-sys-outline-variant, #c7c7d0);
|
||||
border-radius: var(--ui-radius-sm, 0.5rem);
|
||||
transition:
|
||||
border-color 200ms ease,
|
||||
background-color 200ms ease;
|
||||
background: var(--mat-sys-surface-container, #f1f1f5);
|
||||
|
||||
&:not(.editing) {
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
background: var(--ui-panel-bg);
|
||||
color: var(--mat-sys-on-surface);
|
||||
|
||||
&:hover:not(.editing) {
|
||||
border-color: var(--mat-sys-outline, #777680);
|
||||
@@ -63,15 +82,18 @@
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
0 5px 5px -3px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
border-color: var(--mat-sys-primary);
|
||||
background: var(--ui-card-bg);
|
||||
box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 18%);
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
border: 1px dashed var(--mat-sys-primary);
|
||||
background: var(--mat-sys-primary-container);
|
||||
> * {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
@@ -83,90 +105,116 @@
|
||||
}
|
||||
|
||||
.rule-display {
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.priority-statement {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--mat-sys-primary-container, #d7e3ff);
|
||||
color: var(--mat-sys-on-primary-container, #001b3f);
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
color: var(--ui-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-block: 0.25rem 0.75rem;
|
||||
|
||||
mat-icon {
|
||||
color: var(--mat-sys-primary, #005cbb);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-drag-grip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
margin-bottom: -8px;
|
||||
color: var(--mat-sys-outline, #777680);
|
||||
opacity: 0.6;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
border: 1px solid var(--ui-border);
|
||||
border-radius: var(--ui-radius-sm);
|
||||
color: var(--ui-text-secondary);
|
||||
cursor: grab;
|
||||
transition: opacity 0.2s;
|
||||
transition: background-color 150ms ease;
|
||||
|
||||
.rule-item:active & {
|
||||
&:active,
|
||||
.cdk-drag-preview & {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: var(--mat-sys-primary-container);
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
}
|
||||
}
|
||||
|
||||
.rule-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-columns: 2.75rem minmax(0, 1fr);
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 0.75rem;
|
||||
|
||||
mat-slide-toggle {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
.rule-number {
|
||||
color: var(--mat-sys-primary, #005cbb);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rule-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
.rule-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border-top: 1px solid var(--ui-border);
|
||||
padding-top: 0.5rem;
|
||||
|
||||
button {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
padding: 0.625rem;
|
||||
}
|
||||
button:nth-child(3) {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-summary {
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
padding-left: 48px; /* Align with toggle switch text */
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-force {
|
||||
display: inline-block;
|
||||
margin-inline-end: 0.375rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: var(--mat-sys-primary-container);
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
vertical-align: baseline;
|
||||
white-space: nowrap;
|
||||
|
||||
&.obligation {
|
||||
background: var(--ui-warning-bg);
|
||||
color: var(--ui-warning-text);
|
||||
}
|
||||
}
|
||||
|
||||
.rule-editor {
|
||||
padding: 20px;
|
||||
padding: clamp(0.75rem, 3cqi, 1.25rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
@@ -186,23 +234,27 @@
|
||||
align-items: center;
|
||||
|
||||
.action-text {
|
||||
flex-basis: 100%;
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
flex: 1 1 11rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.small-input {
|
||||
width: 100px;
|
||||
flex: 0 1 6.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
@@ -226,17 +278,20 @@
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 24px;
|
||||
border-radius: var(--ui-radius-sm);
|
||||
min-height: 2.75rem;
|
||||
height: auto;
|
||||
padding-block: 0.625rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 30rem) {
|
||||
.rule-summary {
|
||||
padding-left: 0;
|
||||
@container rule-item (max-width: 18rem) {
|
||||
.rule-display {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.rule-header {
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@ import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { RulesPanel } from './rules-panel';
|
||||
import { RuleService, RuleType } from '../../services/rule-service';
|
||||
import { TranslationService } from '../../services/translation-service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop';
|
||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('RulesPanel', () => {
|
||||
let component: RulesPanel;
|
||||
@@ -111,4 +115,31 @@ describe('RulesPanel', () => {
|
||||
component.moveRule(1, -1, { stopPropagation: () => {} } as Event);
|
||||
expect(component.rules[0].id).toBe(secondRuleId);
|
||||
});
|
||||
|
||||
it('keeps cancelled drops unchanged and announces the position after a completed drop', () => {
|
||||
const initialIds = component.rules.map((rule) => rule.id);
|
||||
const movedName = component.rules[0].name;
|
||||
const announce = vi.spyOn(TestBed.inject(LiveAnnouncer), 'announce').mockResolvedValue();
|
||||
const list = fixture.debugElement
|
||||
.query(By.directive(CdkDropList))
|
||||
.injector.get(CdkDropList);
|
||||
const drop = {
|
||||
previousIndex: 0,
|
||||
currentIndex: 1,
|
||||
isPointerOverContainer: false,
|
||||
} as CdkDragDrop<unknown>;
|
||||
list.dropped.emit(drop);
|
||||
expect(component.rules.map((rule) => rule.id)).toEqual(initialIds);
|
||||
expect(announce).not.toHaveBeenCalled();
|
||||
|
||||
list.dropped.emit({ ...drop, isPointerOverContainer: true });
|
||||
expect(component.rules.map((rule) => rule.id)).toEqual([...initialIds].reverse());
|
||||
expect(announce).toHaveBeenCalledWith(`${movedName} : position 2 sur 2.`);
|
||||
// CDK restores the original DOM before emitting dropped. The new order must
|
||||
// be rendered before this callback returns, without waiting for another frame.
|
||||
expect(fixture.nativeElement.querySelectorAll('.rule-name')[1].textContent).toContain(
|
||||
movedName,
|
||||
);
|
||||
expect(fixture.nativeElement.querySelectorAll('.rule-number')[1].textContent).toBe('2.');
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
inject,
|
||||
signal,
|
||||
Injectable,
|
||||
effect,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
@@ -25,37 +25,11 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
||||
|
||||
import {
|
||||
DateAdapter,
|
||||
MAT_DATE_FORMATS,
|
||||
MAT_DATE_LOCALE,
|
||||
NativeDateAdapter,
|
||||
} from '@angular/material/core';
|
||||
import { CdkDragDrop, CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';
|
||||
|
||||
@Injectable()
|
||||
class CustomDateAdapter extends NativeDateAdapter {
|
||||
override format(date: Date, displayFormat: unknown): string {
|
||||
if (displayFormat === 'input') {
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
return super.format(date, displayFormat as object);
|
||||
}
|
||||
}
|
||||
|
||||
const MY_DATE_FORMATS = {
|
||||
parse: { dateInput: 'input' },
|
||||
display: {
|
||||
dateInput: 'input',
|
||||
monthYearLabel: { year: 'numeric', month: 'short' },
|
||||
dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },
|
||||
monthYearA11yLabel: { year: 'numeric', month: 'long' },
|
||||
},
|
||||
};
|
||||
import { DateAdapter } from '@angular/material/core';
|
||||
import { DAY_FIRST_DATE_PROVIDERS } from '../../shared/day-first-date-adapter';
|
||||
import { CdkDragDrop, CdkDropList, CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rules-panel',
|
||||
@@ -71,20 +45,19 @@ const MY_DATE_FORMATS = {
|
||||
FormsModule,
|
||||
CdkDropList,
|
||||
CdkDrag,
|
||||
CdkDragHandle,
|
||||
],
|
||||
templateUrl: './rules-panel.html',
|
||||
styleUrl: './rules-panel.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
{ provide: DateAdapter, useClass: CustomDateAdapter },
|
||||
{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: MY_DATE_FORMATS },
|
||||
],
|
||||
providers: DAY_FIRST_DATE_PROVIDERS,
|
||||
})
|
||||
export class RulesPanel {
|
||||
public ruleService = inject(RuleService);
|
||||
protected readonly translationService = inject(TranslationService);
|
||||
private readonly dateAdapter = inject(DateAdapter);
|
||||
private readonly liveAnnouncer = inject(LiveAnnouncer);
|
||||
private readonly changeDetector = inject(ChangeDetectorRef);
|
||||
|
||||
get tr() {
|
||||
return this.translationService.tr;
|
||||
@@ -145,16 +118,30 @@ export class RulesPanel {
|
||||
}
|
||||
|
||||
public drop(event: CdkDragDrop<VacationRule[]>) {
|
||||
if (event.previousIndex !== event.currentIndex) {
|
||||
this.ruleService.reorderRules(event.previousIndex, event.currentIndex);
|
||||
if (event.isPointerOverContainer && event.previousIndex !== event.currentIndex) {
|
||||
this.reorderRule(event.previousIndex, event.currentIndex);
|
||||
}
|
||||
// CDK restores the original DOM before this callback. Commit the final order
|
||||
// before the next paint so zoneless rendering doesn't flash the old position.
|
||||
this.changeDetector.detectChanges();
|
||||
}
|
||||
|
||||
public moveRule(index: number, offset: -1 | 1, event: Event) {
|
||||
event.stopPropagation();
|
||||
const destination = index + offset;
|
||||
if (destination >= 0 && destination < this.rules.length)
|
||||
this.ruleService.reorderRules(index, destination);
|
||||
this.reorderRule(index, destination);
|
||||
}
|
||||
|
||||
private reorderRule(from: number, to: number) {
|
||||
const rule = this.rules[from];
|
||||
this.ruleService.reorderRules(from, to);
|
||||
void this.liveAnnouncer.announce(
|
||||
this.tr()
|
||||
.rules.ruleMoved.replace('{name}', rule.name)
|
||||
.replace('{position}', String(to + 1))
|
||||
.replace('{count}', String(this.rules.length)),
|
||||
);
|
||||
}
|
||||
|
||||
public toggleRule(id: string) {
|
||||
@@ -229,7 +216,6 @@ export class RulesPanel {
|
||||
|
||||
public getRuleSummary(rule: VacationRule): string {
|
||||
const tr = this.tr();
|
||||
const forceLabel = rule.force === 'obligation' ? tr.rules.obligation : tr.rules.preference;
|
||||
if (rule.type === RuleType.IF_THEN) {
|
||||
const ifThen = rule as IfThenRule;
|
||||
const dayLabel = this.daysOfWeek.find((d) => d.value === ifThen.dayOfWeek)?.label ?? '';
|
||||
@@ -241,13 +227,13 @@ export class RulesPanel {
|
||||
.toLowerCase();
|
||||
const rewardTypeLabel = this.getDayTypeLabel(ifThen.rewardType);
|
||||
|
||||
return `[${forceLabel}] ${tr.rules.ifThenTemplate
|
||||
return tr.rules.ifThenTemplate
|
||||
.replace('{day}', dayLabel)
|
||||
.replace('{placeDays}', String(ifThen.placeDays))
|
||||
.replace('{placeType}', placeTypeLabel)
|
||||
.replace('{placement}', placementLabel)
|
||||
.replace('{rewardDays}', String(ifThen.rewardDays))
|
||||
.replace('{rewardType}', rewardTypeLabel)}`;
|
||||
.replace('{rewardType}', rewardTypeLabel);
|
||||
} else {
|
||||
const timeWindow = rule as TimeWindowRule;
|
||||
const placeTypeLabel = this.getDayTypeLabel(timeWindow.placeType);
|
||||
@@ -258,11 +244,11 @@ export class RulesPanel {
|
||||
this.translationService.locale(),
|
||||
);
|
||||
|
||||
return `[${forceLabel}] ${tr.rules.timeWindowTemplate
|
||||
return tr.rules.timeWindowTemplate
|
||||
.replace('{placeDays}', String(timeWindow.placeDays))
|
||||
.replace('{placeType}', placeTypeLabel)
|
||||
.replace('{startDate}', startDateStr)
|
||||
.replace('{endDate}', endDateStr)}`;
|
||||
.replace('{endDate}', endDateStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user