commit
105f283559
|
@ -1012,4 +1012,22 @@ class behat_app extends behat_app_helper {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View a specific month in the calendar in the app.
|
||||||
|
*
|
||||||
|
* @When /^I open the calendar for "(?P<month>\d+)" "(?P<year>\d+)" in the app$/
|
||||||
|
* @param int $month the month selected as a number
|
||||||
|
* @param int $year the four digit year
|
||||||
|
*/
|
||||||
|
public function i_open_the_calendar_for($month, $year) {
|
||||||
|
$options = json_encode([
|
||||||
|
'params' => [
|
||||||
|
'month' => $month,
|
||||||
|
'year' => $year,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->zone_js("navigator.navigateToSitePath('/calendar/index', $options)");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1695,6 +1695,7 @@
|
||||||
"core.editor.underline": "atto_underline/pluginname",
|
"core.editor.underline": "atto_underline/pluginname",
|
||||||
"core.editor.unorderedlist": "atto_unorderedlist/pluginname",
|
"core.editor.unorderedlist": "atto_unorderedlist/pluginname",
|
||||||
"core.emptysplit": "local_moodlemobileapp",
|
"core.emptysplit": "local_moodlemobileapp",
|
||||||
|
"core.endingtime": "local_moodlemobileapp",
|
||||||
"core.endonesteptour": "tool_usertours",
|
"core.endonesteptour": "tool_usertours",
|
||||||
"core.error": "moodle",
|
"core.error": "moodle",
|
||||||
"core.errorchangecompletion": "local_moodlemobileapp",
|
"core.errorchangecompletion": "local_moodlemobileapp",
|
||||||
|
@ -2304,6 +2305,7 @@
|
||||||
"core.sort": "moodle",
|
"core.sort": "moodle",
|
||||||
"core.sortby": "moodle",
|
"core.sortby": "moodle",
|
||||||
"core.start": "local_moodlemobileapp",
|
"core.start": "local_moodlemobileapp",
|
||||||
|
"core.startingtime": "local_moodlemobileapp",
|
||||||
"core.storingfiles": "local_moodlemobileapp",
|
"core.storingfiles": "local_moodlemobileapp",
|
||||||
"core.strftimedate": "langconfig",
|
"core.strftimedate": "langconfig",
|
||||||
"core.strftimedatefullshort": "langconfig",
|
"core.strftimedatefullshort": "langconfig",
|
||||||
|
|
|
@ -257,27 +257,9 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
* Go to current month.
|
* Go to current month.
|
||||||
*/
|
*/
|
||||||
async goToCurrentMonth(): Promise<void> {
|
async goToCurrentMonth(): Promise<void> {
|
||||||
const manager = this.manager;
|
const currentMoment = moment();
|
||||||
const slides = this.slides;
|
|
||||||
if (!manager || !slides) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentMonth = {
|
await this.viewMonth(currentMoment.month() + 1, currentMoment.year());
|
||||||
moment: moment(),
|
|
||||||
};
|
|
||||||
this.loaded = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Make sure the day is loaded.
|
|
||||||
await manager.getSource().loadItem(currentMonth);
|
|
||||||
|
|
||||||
slides.slideToItem(currentMonth);
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
|
||||||
} finally {
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -319,6 +301,39 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View a certain month and year.
|
||||||
|
*
|
||||||
|
* @param month Month.
|
||||||
|
* @param year Year.
|
||||||
|
*/
|
||||||
|
async viewMonth(month: number, year: number): Promise<void> {
|
||||||
|
const manager = this.manager;
|
||||||
|
const slides = this.slides;
|
||||||
|
if (!manager || !slides) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loaded = false;
|
||||||
|
const item = {
|
||||||
|
moment: moment({
|
||||||
|
year,
|
||||||
|
month: month - 1,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Make sure the day is loaded.
|
||||||
|
await manager.getSource().loadItem(item);
|
||||||
|
|
||||||
|
slides.slideToItem(item);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component destroyed.
|
* Component destroyed.
|
||||||
*/
|
*/
|
||||||
|
@ -511,6 +526,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
|
||||||
const weeks = result.weeks as AddonCalendarWeek[];
|
const weeks = result.weeks as AddonCalendarWeek[];
|
||||||
const currentDay = moment().date();
|
const currentDay = moment().date();
|
||||||
const currentTime = CoreTimeUtils.timestamp();
|
const currentTime = CoreTimeUtils.timestamp();
|
||||||
|
const dayMoment = moment(month.moment);
|
||||||
|
|
||||||
const preloadedMonth: PreloadedMonth = {
|
const preloadedMonth: PreloadedMonth = {
|
||||||
...month,
|
...month,
|
||||||
|
@ -523,7 +539,7 @@ class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicI
|
||||||
await Promise.all(weeks.map(async (week) => {
|
await Promise.all(weeks.map(async (week) => {
|
||||||
await Promise.all(week.days.map(async (day) => {
|
await Promise.all(week.days.map(async (day) => {
|
||||||
day.periodName = CoreTimeUtils.userDate(
|
day.periodName = CoreTimeUtils.userDate(
|
||||||
month.moment.unix() * 1000,
|
dayMoment.date(day.mday).unix() * 1000,
|
||||||
'core.strftimedaydate',
|
'core.strftimedaydate',
|
||||||
);
|
);
|
||||||
day.eventsFormated = day.eventsFormated || [];
|
day.eventsFormated = day.eventsFormated || [];
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<p class="item-heading" [core-mark-required]="true">{{ 'core.date' | translate }}</p>
|
<p class="item-heading" [core-mark-required]="true">{{ 'core.date' | translate }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-datetime formControlName="timestart" [placeholder]="'core.date' | translate" [displayFormat]="dateFormat"
|
<ion-datetime formControlName="timestart" [placeholder]="'core.date' | translate" [displayFormat]="dateFormat"
|
||||||
[max]="maxDate" [min]="minDate" display-timezone="utc">
|
[max]="maxDate" [min]="minDate" [displayTimezone]="displayTimezone">
|
||||||
</ion-datetime>
|
</ion-datetime>
|
||||||
<core-input-errors [control]="form.controls.timestart" [errorMessages]="errors"></core-input-errors>
|
<core-input-errors [control]="form.controls.timestart" [errorMessages]="errors"></core-input-errors>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
@ -156,7 +156,8 @@
|
||||||
<ion-item *ngIf="form.controls.duration.value === 1">
|
<ion-item *ngIf="form.controls.duration.value === 1">
|
||||||
<ion-label position="stacked"></ion-label>
|
<ion-label position="stacked"></ion-label>
|
||||||
<ion-datetime formControlName="timedurationuntil" [max]="maxDate" [min]="minDate"
|
<ion-datetime formControlName="timedurationuntil" [max]="maxDate" [min]="minDate"
|
||||||
[placeholder]="'addon.calendar.durationuntil' | translate" [displayFormat]="dateFormat" display-timezone="utc">
|
[placeholder]="'addon.calendar.durationuntil' | translate" [displayFormat]="dateFormat"
|
||||||
|
[displayTimezone]="displayTimezone">
|
||||||
</ion-datetime>
|
</ion-datetime>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
|
|
|
@ -45,6 +45,8 @@ import { CanLeave } from '@guards/can-leave';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
import { CoreReminders, CoreRemindersService, CoreRemindersUnits } from '@features/reminders/services/reminders';
|
import { CoreReminders, CoreRemindersService, CoreRemindersUnits } from '@features/reminders/services/reminders';
|
||||||
import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu';
|
import { CoreRemindersSetReminderMenuComponent } from '@features/reminders/components/set-reminder-menu/set-reminder-menu';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
import { CoreAppProvider } from '@services/app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays a form to create/edit an event.
|
* Page that displays a form to create/edit an event.
|
||||||
|
@ -77,6 +79,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
eventId?: number;
|
eventId?: number;
|
||||||
maxDate: string;
|
maxDate: string;
|
||||||
minDate: string;
|
minDate: string;
|
||||||
|
displayTimezone?: string;
|
||||||
|
|
||||||
// Form variables.
|
// Form variables.
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
|
@ -108,6 +111,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
// Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
|
// Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
|
||||||
this.dateFormat = CoreTimeUtils.convertPHPToMoment(Translate.instant('core.strftimedatetimeshort'))
|
this.dateFormat = CoreTimeUtils.convertPHPToMoment(Translate.instant('core.strftimedatetimeshort'))
|
||||||
.replace(/[[\]]/g, '');
|
.replace(/[[\]]/g, '');
|
||||||
|
this.displayTimezone = CoreAppProvider.getForcedTimezone();
|
||||||
|
|
||||||
this.form = new FormGroup({});
|
this.form = new FormGroup({});
|
||||||
|
|
||||||
|
@ -454,8 +458,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
async submit(): Promise<void> {
|
async submit(): Promise<void> {
|
||||||
// Validate data.
|
// Validate data.
|
||||||
const formData = this.form.value;
|
const formData = this.form.value;
|
||||||
const timeStartDate = CoreTimeUtils.convertToTimestamp(formData.timestart, true);
|
const timeStartDate = moment(formData.timestart).unix();
|
||||||
const timeUntilDate = CoreTimeUtils.convertToTimestamp(formData.timedurationuntil, true);
|
const timeUntilDate = moment(formData.timedurationuntil).unix();
|
||||||
const timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10);
|
const timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10);
|
||||||
let error: string | undefined;
|
let error: string | undefined;
|
||||||
|
|
||||||
|
@ -488,6 +492,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy, CanLeave {
|
||||||
description: {
|
description: {
|
||||||
text: formData.description || '',
|
text: formData.description || '',
|
||||||
format: 1,
|
format: 1,
|
||||||
|
itemid: 0, // Files not supported yet.
|
||||||
},
|
},
|
||||||
location: formData.location,
|
location: formData.location,
|
||||||
duration: formData.duration,
|
duration: formData.duration,
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
</h1>
|
</h1>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.calendar.when' | translate }}</h2>
|
<h2>{{ 'addon.calendar.when' | translate }}</h2>
|
||||||
<core-format-text [text]="event.formattedtime" [contextLevel]="event.contextLevel"
|
<core-format-text [text]="event.formattedtime" [contextLevel]="event.contextLevel"
|
||||||
|
|
|
@ -173,6 +173,10 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
this.filter.filtered = !!this.filter.courseId;
|
this.filter.filtered = !!this.filter.courseId;
|
||||||
|
|
||||||
this.fetchData(true, false);
|
this.fetchData(true, false);
|
||||||
|
|
||||||
|
if (this.year !== undefined && this.month !== undefined && this.calendarComponent) {
|
||||||
|
this.calendarComponent.viewMonth(this.month, this.year);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const deepLinkManager = new CoreMainMenuDeepLinkManager();
|
const deepLinkManager = new CoreMainMenuDeepLinkManager();
|
||||||
|
|
|
@ -253,6 +253,7 @@ export class AddonCalendarSyncProvider extends CoreSyncBaseProvider<AddonCalenda
|
||||||
description: {
|
description: {
|
||||||
text: event.description || '',
|
text: event.description || '',
|
||||||
format: 1,
|
format: 1,
|
||||||
|
itemid: 0, // Files not supported yet.
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
); // Clone the object because it will be modified in the submit function.
|
); // Clone the object because it will be modified in the submit function.
|
||||||
|
|
|
@ -345,19 +345,24 @@ export class AddonCalendarProvider {
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
|
||||||
|
const getTimeHtml = (time: string, a11yLangKey: string): string =>
|
||||||
|
`<span aria-label="${Translate.instant(a11yLangKey, { $a: CoreTextUtils.cleanTags(time) })}">${time}</span>`;
|
||||||
|
const getStartTimeHtml = (time: string): string => getTimeHtml(time, 'core.startingtime');
|
||||||
|
const getEndTimeHtml = (time: string): string => getTimeHtml(time, 'core.endingtime');
|
||||||
|
|
||||||
const start = event.timestart * 1000;
|
const start = event.timestart * 1000;
|
||||||
const end = (event.timestart + event.timeduration) * 1000;
|
const end = (event.timestart + event.timeduration) * 1000;
|
||||||
let time: string;
|
let time: string;
|
||||||
|
|
||||||
if (!event.timeduration) {
|
if (event.timeduration) {
|
||||||
|
|
||||||
if (moment(start).isSame(end, 'day')) {
|
if (moment(start).isSame(end, 'day')) {
|
||||||
// Event starts and ends the same day.
|
// Event starts and ends the same day.
|
||||||
if (event.timeduration == CoreConstants.SECONDS_DAY) {
|
if (event.timeduration == CoreConstants.SECONDS_DAY) {
|
||||||
time = Translate.instant('addon.calendar.allday');
|
time = Translate.instant('addon.calendar.allday');
|
||||||
} else {
|
} else {
|
||||||
time = CoreTimeUtils.userDate(start, format) + ' <strong>»</strong> ' +
|
time = getStartTimeHtml(CoreTimeUtils.userDate(start, format)) + ' <strong>»</strong> ' +
|
||||||
CoreTimeUtils.userDate(end, format);
|
getEndTimeHtml(CoreTimeUtils.userDate(end, format));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -388,11 +393,12 @@ export class AddonCalendarProvider {
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
return dayStart + timeStart + ' <strong>»</strong> ' + dayEnd + timeEnd;
|
return getStartTimeHtml(dayStart + timeStart) + ' <strong>»</strong> ' +
|
||||||
|
getEndTimeHtml(dayEnd + timeEnd);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// There is no time duration.
|
// There is no time duration.
|
||||||
time = CoreTimeUtils.userDate(start, format);
|
time = getStartTimeHtml(CoreTimeUtils.userDate(start, format));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showTime) {
|
if (showTime) {
|
||||||
|
@ -2173,6 +2179,7 @@ export type AddonCalendarSubmitCreateUpdateFormDataWSParams = Omit<AddonCalendar
|
||||||
description?: {
|
description?: {
|
||||||
text: string;
|
text: string;
|
||||||
format: number;
|
format: number;
|
||||||
|
itemid: number; // File area ID.
|
||||||
};
|
};
|
||||||
visible?: number;
|
visible?: number;
|
||||||
instance?: number;
|
instance?: number;
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
@core @core_calendar @app @javascript
|
||||||
|
Feature: Test creation of calendar events in app
|
||||||
|
In order to take advantage of all the calendar features while using the mobile app
|
||||||
|
As a student
|
||||||
|
I need basic to be able to create and edit calendar events in the app
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| teacher1 | Teacher | teacher | teacher1@example.com |
|
||||||
|
| student1 | Student1 | student1 | student1@example.com |
|
||||||
|
And the following "courses" exist:
|
||||||
|
| fullname | shortname | category |
|
||||||
|
| Course 1 | C1 | 0 |
|
||||||
|
And the following "course enrolments" exist:
|
||||||
|
| user | course | role |
|
||||||
|
| teacher1 | C1 | editingteacher |
|
||||||
|
| student1 | C1 | student |
|
||||||
|
|
||||||
|
Scenario: Create user event as student from monthly view
|
||||||
|
Given I entered the app as "student1"
|
||||||
|
When I press "More" in the app
|
||||||
|
And I press "Calendar" in the app
|
||||||
|
And I press "New event" in the app
|
||||||
|
Then the field "Date" matches value "## now ##%d/%m/%y, %H:%M##" in the app
|
||||||
|
And I should not be able to press "Save" in the app
|
||||||
|
|
||||||
|
# Check that student can only create User events.
|
||||||
|
When I press "Type of event" in the app
|
||||||
|
Then I should not find "Cancel" in the app
|
||||||
|
And I should find "User" within "Type of event" "ion-item" in the app
|
||||||
|
|
||||||
|
# Create the event.
|
||||||
|
When I set the field "Event title" to "User Event 01" in the app
|
||||||
|
And I set the field "Date" to "2025-04-11T09:00+08:00" in the app
|
||||||
|
And I press "Without duration" in the app
|
||||||
|
And I set the field "Description" to "This is User Event 01 description." in the app
|
||||||
|
And I set the field "Location" to "Barcelona" in the app
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "Calendar events" in the app
|
||||||
|
|
||||||
|
# Verify that event was created right.
|
||||||
|
When I open the calendar for "4" "2025" in the app
|
||||||
|
And I press "Friday, 11 April 2025" in the app
|
||||||
|
Then I should find "User Event 01" in the app
|
||||||
|
|
||||||
|
When I press "User Event 01" in the app
|
||||||
|
Then I should find "Friday, 11 April" in the app
|
||||||
|
And I should find "Starting time: 9:00 AM" in the app
|
||||||
|
And I should find "User event" within "Event type" "ion-item" in the app
|
||||||
|
And I should find "This is User Event 01 description." in the app
|
||||||
|
And I should find "Barcelona" in the app
|
||||||
|
But I should not find "Ending time" in the app
|
||||||
|
|
||||||
|
When I press "Display options" in the app
|
||||||
|
Then I should find "Edit" in the app
|
||||||
|
And I should find "Delete" in the app
|
||||||
|
|
||||||
|
When I close the popup in the app
|
||||||
|
And I press "Barcelona" in the app
|
||||||
|
And I press "OK" in the app
|
||||||
|
Then the app should have opened a browser tab with url "google.com"
|
||||||
|
|
||||||
|
# @todo: Add more Scenarios to test teacher, different values, and creating events from other views (e.g. day view).
|
|
@ -1,7 +1,7 @@
|
||||||
<span *ngIf="inputMode && form" [formGroup]="form">
|
<span *ngIf="inputMode && form" [formGroup]="form">
|
||||||
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
|
<span *ngIf="editMode" [core-mark-required]="field.required" class="core-mark-required"></span>
|
||||||
<ion-datetime [formControlName]="'f_'+field.id" [placeholder]="'core.date' | translate" [max]="maxDate" [min]="minDate"
|
<ion-datetime [formControlName]="'f_'+field.id" [placeholder]="'core.date' | translate" [max]="maxDate" [min]="minDate"
|
||||||
[disabled]="searchMode && !searchFields!['f_'+field.id+'_z']" [displayFormat]="format" display-timezone="utc">
|
[disabled]="searchMode && !searchFields!['f_'+field.id+'_z']" [displayFormat]="format" [displayTimezone]="displayTimezone">
|
||||||
</ion-datetime>
|
</ion-datetime>
|
||||||
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
|
<core-input-errors *ngIf="error && editMode" [control]="form.controls['f_'+field.id]" [errorText]="error"></core-input-errors>
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { CoreAppProvider } from '@services/app';
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import moment, { Moment } from 'moment-timezone';
|
import moment, { Moment } from 'moment-timezone';
|
||||||
|
@ -31,6 +32,7 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginBaseC
|
||||||
displayDate?: number;
|
displayDate?: number;
|
||||||
maxDate?: string;
|
maxDate?: string;
|
||||||
minDate?: string;
|
minDate?: string;
|
||||||
|
displayTimezone?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
@ -52,6 +54,7 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginBaseC
|
||||||
));
|
));
|
||||||
this.maxDate = CoreTimeUtils.getDatetimeDefaultMax();
|
this.maxDate = CoreTimeUtils.getDatetimeDefaultMax();
|
||||||
this.minDate = CoreTimeUtils.getDatetimeDefaultMin();
|
this.minDate = CoreTimeUtils.getDatetimeDefaultMin();
|
||||||
|
this.displayTimezone = CoreAppProvider.getForcedTimezone();
|
||||||
|
|
||||||
if (this.searchMode) {
|
if (this.searchMode) {
|
||||||
this.addControl('f_' + this.field.id + '_z');
|
this.addControl('f_' + this.field.id + '_z');
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<span [core-mark-required]="required">{{ field.name }}</span>
|
<span [core-mark-required]="required">{{ field.name }}</span>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-datetime [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="format" [max]="max"
|
<ion-datetime [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="format" [max]="max"
|
||||||
[min]="min" [monthNames]="monthNames">
|
[min]="min" [monthNames]="monthNames" [displayTimezone]="displayTimezone">
|
||||||
</ion-datetime>
|
</ion-datetime>
|
||||||
<core-input-errors [control]="form.controls[modelName]"></core-input-errors>
|
<core-input-errors [control]="form.controls[modelName]"></core-input-errors>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { CoreUserProfileField } from '@features/user/services/user';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
|
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
|
||||||
import { CoreLang } from '@services/lang';
|
import { CoreLang } from '@services/lang';
|
||||||
|
import { CoreAppProvider } from '@services/app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to render a datetime user profile field.
|
* Directive to render a datetime user profile field.
|
||||||
|
@ -37,6 +38,7 @@ export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileField
|
||||||
max?: string;
|
max?: string;
|
||||||
valueNumber = 0;
|
valueNumber = 0;
|
||||||
monthNames?: string[];
|
monthNames?: string[];
|
||||||
|
displayTimezone?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init the data when the field is meant to be displayed without editing.
|
* Init the data when the field is meant to be displayed without editing.
|
||||||
|
@ -56,6 +58,7 @@ export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileField
|
||||||
super.initForEdit(field);
|
super.initForEdit(field);
|
||||||
|
|
||||||
this.monthNames = CoreLang.getMonthNames();
|
this.monthNames = CoreLang.getMonthNames();
|
||||||
|
this.displayTimezone = CoreAppProvider.getForcedTimezone();
|
||||||
|
|
||||||
// Check if it's only date or it has time too.
|
// Check if it's only date or it has time too.
|
||||||
const hasTime = CoreUtils.isTrueOrOne(field.param3);
|
const hasTime = CoreUtils.isTrueOrOne(field.param3);
|
||||||
|
|
|
@ -18,9 +18,9 @@ import { AuthEmailSignupProfileField } from '@features/login/services/login-help
|
||||||
import { CoreUserProfileField } from '@features/user/services/user';
|
import { CoreUserProfileField } from '@features/user/services/user';
|
||||||
import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate';
|
import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate';
|
||||||
import { CoreFormFields } from '@singletons/form';
|
import { CoreFormFields } from '@singletons/form';
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import { AddonUserProfileFieldDatetimeComponent } from '../../component/datetime';
|
import { AddonUserProfileFieldDatetimeComponent } from '../../component/datetime';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datetime user profile field handlers.
|
* Datetime user profile field handlers.
|
||||||
|
@ -61,7 +61,7 @@ export class AddonUserProfileFieldDatetimeHandlerService implements CoreUserProf
|
||||||
return {
|
return {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
name: 'profile_field_' + field.shortname,
|
name: 'profile_field_' + field.shortname,
|
||||||
value: CoreTimeUtils.convertToTimestamp(<string> formValues[name]),
|
value: moment(<string> formValues[name]).unix(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager';
|
import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager';
|
||||||
import { IonContent, IonSlides } from '@ionic/angular';
|
import { IonContent, IonSlides } from '@ionic/angular';
|
||||||
import { CoreDomUtils, VerticalPoint } from '@services/utils/dom';
|
import { CoreDomUtils, VerticalPoint } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreDom } from '@singletons/dom';
|
import { CoreDom } from '@singletons/dom';
|
||||||
import { CoreEventObserver } from '@singletons/events';
|
import { CoreEventObserver } from '@singletons/events';
|
||||||
import { CoreMath } from '@singletons/math';
|
import { CoreMath } from '@singletons/math';
|
||||||
|
@ -43,6 +44,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
||||||
protected hostElement: HTMLElement;
|
protected hostElement: HTMLElement;
|
||||||
protected unsubscribe?: () => void;
|
protected unsubscribe?: () => void;
|
||||||
protected resizeListener: CoreEventObserver;
|
protected resizeListener: CoreEventObserver;
|
||||||
|
protected updateSlidesPromise?: Promise<void>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
elementRef: ElementRef<HTMLElement>,
|
elementRef: ElementRef<HTMLElement>,
|
||||||
|
@ -51,7 +53,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
||||||
this.hostElement = elementRef.nativeElement;
|
this.hostElement = elementRef.nativeElement;
|
||||||
|
|
||||||
this.resizeListener = CoreDom.onWindowResize(() => {
|
this.resizeListener = CoreDom.onWindowResize(() => {
|
||||||
this.slides?.update();
|
this.updateSlidesComponent();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +120,22 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
||||||
* @param speed Animation speed.
|
* @param speed Animation speed.
|
||||||
* @param runCallbacks Whether to run callbacks.
|
* @param runCallbacks Whether to run callbacks.
|
||||||
*/
|
*/
|
||||||
slideToIndex(index: number, speed?: number, runCallbacks?: boolean): void {
|
async slideToIndex(index: number, speed?: number, runCallbacks?: boolean): Promise<void> {
|
||||||
|
// If slides are being updated, wait for the update to finish.
|
||||||
|
await this.updateSlidesPromise;
|
||||||
|
|
||||||
|
const slides = this.slides;
|
||||||
|
if (!slides) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the number of slides matches the number of items.
|
||||||
|
const slidesLength = await slides.length();
|
||||||
|
if (slidesLength !== this.items.length) {
|
||||||
|
// Number doesn't match, do a new update to try to match them.
|
||||||
|
await this.updateSlidesComponent();
|
||||||
|
}
|
||||||
|
|
||||||
this.slides?.slideTo(index, speed, runCallbacks);
|
this.slides?.slideTo(index, speed, runCallbacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +149,7 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
||||||
slideToItem(item: Item, speed?: number, runCallbacks?: boolean): void {
|
slideToItem(item: Item, speed?: number, runCallbacks?: boolean): void {
|
||||||
const index = this.manager?.getSource().getItemIndex(item) ?? -1;
|
const index = this.manager?.getSource().getItemIndex(item) ?? -1;
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
this.slides?.slideTo(index, speed, runCallbacks);
|
this.slideToIndex(index, speed, runCallbacks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,10 +175,14 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when items list has been updated.
|
* Called when items list has been updated.
|
||||||
*
|
|
||||||
* @param items New items.
|
|
||||||
*/
|
*/
|
||||||
protected onItemsUpdated(): void {
|
protected async onItemsUpdated(): Promise<void> {
|
||||||
|
// Wait for slides to be added in DOM.
|
||||||
|
await CoreUtils.nextTick();
|
||||||
|
|
||||||
|
// Update the slides component so the slides list reflects the new items.
|
||||||
|
await this.updateSlidesComponent();
|
||||||
|
|
||||||
const currentItem = this.manager?.getSelectedItem();
|
const currentItem = this.manager?.getSelectedItem();
|
||||||
|
|
||||||
if (!currentItem || !this.manager) {
|
if (!currentItem || !this.manager) {
|
||||||
|
@ -249,6 +270,24 @@ export class CoreSwipeSlidesComponent<Item = unknown> implements OnChanges, OnDe
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update slides component.
|
||||||
|
*/
|
||||||
|
protected async updateSlidesComponent(): Promise<void> {
|
||||||
|
if (!this.slides) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = this.slides.update();
|
||||||
|
this.updateSlidesPromise = promise;
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
if (this.updateSlidesPromise === promise) {
|
||||||
|
delete this.updateSlidesPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
"downloading": "Downloading",
|
"downloading": "Downloading",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"emptysplit": "This page will appear blank if the left panel is empty or is loading.",
|
"emptysplit": "This page will appear blank if the left panel is empty or is loading.",
|
||||||
|
"endingtime": "Ending time: {{$a}}",
|
||||||
"endonesteptour": "Got it",
|
"endonesteptour": "Got it",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"errorchangecompletion": "An error occurred while changing the completion status. Please try again.",
|
"errorchangecompletion": "An error occurred while changing the completion status. Please try again.",
|
||||||
|
@ -294,6 +295,7 @@
|
||||||
"sort": "Sort",
|
"sort": "Sort",
|
||||||
"sortby": "Sort by",
|
"sortby": "Sort by",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
|
"startingtime": "Starting time: {{$a}}",
|
||||||
"storingfiles": "Storing files",
|
"storingfiles": "Storing files",
|
||||||
"strftimedate": "%d %B %Y",
|
"strftimedate": "%d %B %Y",
|
||||||
"strftimedatefullshort": "%d/%m/%y",
|
"strftimedatefullshort": "%d/%m/%y",
|
||||||
|
|
|
@ -70,6 +70,18 @@ export class CoreAppProvider {
|
||||||
return !!navigator.webdriver;
|
return !!navigator.webdriver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the forced timezone to use. Timezone is forced for automated tests.
|
||||||
|
*
|
||||||
|
* @return Timezone. Undefined to use the user's timezone.
|
||||||
|
*/
|
||||||
|
static getForcedTimezone(): string | undefined {
|
||||||
|
if (CoreAppProvider.isAutomated()) {
|
||||||
|
// Use the same timezone forced for LMS in tests.
|
||||||
|
return 'Australia/Perth';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize database.
|
* Initialize database.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -261,23 +261,14 @@ export class CoreTimeUtilsProvider {
|
||||||
/**
|
/**
|
||||||
* Convert a text into user timezone timestamp.
|
* Convert a text into user timezone timestamp.
|
||||||
*
|
*
|
||||||
* @todo The `applyOffset` argument is only used as a workaround, it should be removed once
|
|
||||||
* MOBILE-3784 is resolved.
|
|
||||||
*
|
|
||||||
* @param date To convert to timestamp.
|
* @param date To convert to timestamp.
|
||||||
* @param applyOffset Whether to apply offset to date or not.
|
* @param applyOffset Whether to apply offset to date or not.
|
||||||
* @return Converted timestamp.
|
* @return Converted timestamp.
|
||||||
|
* @deprecated since 4.1. Use moment(date).unix() instead.
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
convertToTimestamp(date: string, applyOffset?: boolean): number {
|
convertToTimestamp(date: string, applyOffset?: boolean): number {
|
||||||
const timestamp = moment(date).unix();
|
return moment(date).unix();
|
||||||
|
|
||||||
if (applyOffset !== undefined) {
|
|
||||||
return applyOffset ? timestamp - moment().utcOffset() * 60 : timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeof date == 'string' && date.slice(-1) == 'Z'
|
|
||||||
? timestamp - moment().utcOffset() * 60
|
|
||||||
: timestamp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -192,9 +192,25 @@ export class TestingBehatBlockingService {
|
||||||
*/
|
*/
|
||||||
protected async checkUIBlocked(): Promise<void> {
|
protected async checkUIBlocked(): Promise<void> {
|
||||||
await CoreUtils.nextTick();
|
await CoreUtils.nextTick();
|
||||||
const blocked = document.querySelector<HTMLElement>('div.core-loading-container, ion-loading, .click-block-active');
|
|
||||||
|
|
||||||
if (blocked?.offsetParent) {
|
const blockingElements = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('div.core-loading-container, ion-loading, .click-block-active'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isBlocked = blockingElements.some(element => {
|
||||||
|
if (!element.offsetParent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slide = element.closest('ion-slide');
|
||||||
|
if (slide && !slide.classList.contains('swiper-slide-active')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isBlocked) {
|
||||||
if (!this.waitingBlocked) {
|
if (!this.waitingBlocked) {
|
||||||
this.block('blocked');
|
this.block('blocked');
|
||||||
this.waitingBlocked = true;
|
this.waitingBlocked = true;
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { CoreComponentsRegistry } from '@singletons/components-registry';
|
||||||
import { CoreDom } from '@singletons/dom';
|
import { CoreDom } from '@singletons/dom';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreSites, CoreSitesProvider } from '@services/sites';
|
import { CoreSites, CoreSitesProvider } from '@services/sites';
|
||||||
|
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Behat runtime servive with public API.
|
* Behat runtime servive with public API.
|
||||||
|
@ -56,6 +57,10 @@ export class TestingBehatRuntimeService {
|
||||||
return CoreSites.instance;
|
return CoreSites.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get navigator(): CoreNavigatorService {
|
||||||
|
return CoreNavigator.instance;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init behat functions and set options like skipping onboarding.
|
* Init behat functions and set options like skipping onboarding.
|
||||||
*
|
*
|
||||||
|
@ -436,7 +441,7 @@ export class TestingBehatRuntimeService {
|
||||||
return 'ERROR: No element matches field to set.';
|
return 'ERROR: No element matches field to set.';
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundValue = 'value' in found ? found.value : found.innerText;
|
const foundValue = this.getFieldValue(found);
|
||||||
if (value !== foundValue) {
|
if (value !== foundValue) {
|
||||||
return `ERROR: Expecting value "${value}", found "${foundValue}" instead.`;
|
return `ERROR: Expecting value "${value}", found "${foundValue}" instead.`;
|
||||||
}
|
}
|
||||||
|
@ -457,6 +462,24 @@ export class TestingBehatRuntimeService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of a certain field.
|
||||||
|
*
|
||||||
|
* @param element Field to get the value.
|
||||||
|
* @return Value.
|
||||||
|
*/
|
||||||
|
protected getFieldValue(element: HTMLElement | HTMLInputElement): string {
|
||||||
|
if (element.tagName === 'ION-DATETIME') {
|
||||||
|
// ion-datetime's value is a timestamp in ISO format. Use the text displayed to the user instead.
|
||||||
|
const dateTimeTextElement = element.shadowRoot?.querySelector<HTMLElement>('.datetime-text');
|
||||||
|
if (dateTimeTextElement) {
|
||||||
|
return dateTimeTextElement.innerText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'value' in element ? element.value : element.innerText;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an Angular component instance.
|
* Get an Angular component instance.
|
||||||
*
|
*
|
||||||
|
|
|
@ -28,8 +28,8 @@ function initializeAutomatedTests(window: AutomatedTestsWindow) {
|
||||||
|
|
||||||
window.behat = TestingBehatRuntime.instance;
|
window.behat = TestingBehatRuntime.instance;
|
||||||
|
|
||||||
// Force timezone for automated tests. Use the same timezone forced for LMS in tests.
|
// Force timezone for automated tests.
|
||||||
moment.tz.setDefault('Australia/Perth');
|
moment.tz.setDefault(CoreAppProvider.getForcedTimezone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
Loading…
Reference in New Issue