Merge pull request #3447 from dpalou/MOBILE-3784

Mobile 3784
main
Noel De Martin 2022-11-16 16:27:26 +01:00 committed by GitHub
commit 105f283559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 265 additions and 58 deletions

View File

@ -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)");
}
} }

View File

@ -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",

View File

@ -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 || [];

View File

@ -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>

View File

@ -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,

View File

@ -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"

View File

@ -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();

View File

@ -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.

View File

@ -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>&raquo;</strong> ' + time = getStartTimeHtml(CoreTimeUtils.userDate(start, format)) + ' <strong>&raquo;</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>&raquo;</strong> ' + dayEnd + timeEnd; return getStartTimeHtml(dayStart + timeStart) + ' <strong>&raquo;</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;

View File

@ -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).

View File

@ -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>

View File

@ -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');

View File

@ -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>

View File

@ -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);

View File

@ -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(),
}; };
} }
} }

View File

@ -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
*/ */

View File

@ -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",

View File

@ -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.
*/ */

View File

@ -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;
} }
/** /**

View File

@ -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;

View File

@ -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.
* *

View File

@ -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({