MOBILE-3806 myoverview: Add layout selector
parent
29f6d6cd39
commit
69584e361c
|
@ -4,20 +4,19 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<div slot="end" class="flex-row">
|
<div slot="end" class="flex-row">
|
||||||
<!-- Download all courses. -->
|
<!-- Download all courses. -->
|
||||||
<div *ngIf="downloadCoursesEnabled && downloadEnabled && filteredCourses.length > 1 && !showFilter"
|
<div *ngIf="downloadCoursesEnabled && downloadEnabled && filteredCourses.length > 1 && !showFilter" class="core-button-spinner">
|
||||||
class="core-button-spinner">
|
<ion-button *ngIf="!prefetchCoursesData[timeSelectorFilter].loading" fill="clear" color="dark" (click)="prefetchCourses()"
|
||||||
<ion-button *ngIf="!prefetchCoursesData[selectedFilter].loading" fill="clear" color="dark" (click)="prefetchCourses()"
|
|
||||||
[attr.aria-label]="'core.courses.downloadcourses' | translate">
|
[attr.aria-label]="'core.courses.downloadcourses' | translate">
|
||||||
<ion-icon [name]="prefetchCoursesData[selectedFilter].icon" slot="icon-only" aria-hidden="true">
|
<ion-icon [name]="prefetchCoursesData[timeSelectorFilter].icon" slot="icon-only" aria-hidden="true">
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData[selectedFilter].badge"
|
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData[timeSelectorFilter].badge"
|
||||||
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData[selectedFilter].total"
|
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData[timeSelectorFilter].total"
|
||||||
[attr.aria-valuenow]="prefetchCoursesData[selectedFilter].count"
|
[attr.aria-valuenow]="prefetchCoursesData[timeSelectorFilter].count"
|
||||||
[attr.aria-valuetext]="prefetchCoursesData[selectedFilter].badgeA11yText">
|
[attr.aria-valuetext]="prefetchCoursesData[timeSelectorFilter].badgeA11yText">
|
||||||
{{prefetchCoursesData[selectedFilter].badge}}
|
{{prefetchCoursesData[timeSelectorFilter].badge}}
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
<ion-spinner *ngIf="prefetchCoursesData[selectedFilter].loading" [attr.aria-label]="'core.loading' | translate">
|
<ion-spinner *ngIf="prefetchCoursesData[timeSelectorFilter].loading" [attr.aria-label]="'core.loading' | translate">
|
||||||
</ion-spinner>
|
</ion-spinner>
|
||||||
</div>
|
</div>
|
||||||
<core-context-menu>
|
<core-context-menu>
|
||||||
|
@ -40,9 +39,9 @@
|
||||||
</div>
|
</div>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<core-loading [hideUntil]="loaded" [fullscreen]="false">
|
<core-loading [hideUntil]="loaded" [fullscreen]="false">
|
||||||
<div class="safe-area-padding-horizontal" [hidden]="showFilter || !showSelectorFilter">
|
<div class="safe-area-padding-horizontal" [hidden]="showFilter || !showTimeSelectorFilter">
|
||||||
<!-- "Time" selector. -->
|
<!-- "Time" selector. -->
|
||||||
<core-combobox [label]="'core.show' | translate" [selection]="selectedFilter" (onChange)="selectedChanged($event)">
|
<core-combobox [label]="'core.show' | translate" [selection]="timeSelectorFilter" (onChange)="timeSelectorChanged($event)">
|
||||||
<ion-select-option class="ion-text-wrap" value="allincludinghidden" *ngIf="showFilters.allincludinghidden != 'hidden'">
|
<ion-select-option class="ion-text-wrap" value="allincludinghidden" *ngIf="showFilters.allincludinghidden != 'hidden'">
|
||||||
{{ 'addon.block_myoverview.allincludinghidden' | translate }}
|
{{ 'addon.block_myoverview.allincludinghidden' | translate }}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
|
@ -77,6 +76,16 @@
|
||||||
</core-combobox>
|
</core-combobox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="safe-area-padding-horizontal" [hidden]="showFilter || layouts.length <= 1">
|
||||||
|
<!-- "Layouts" selector. -->
|
||||||
|
<core-combobox [label]="'core.show' | translate" [selection]="selectedLayout" (onChange)="layoutChanged($event)">
|
||||||
|
<ng-container *ngFor="let layout of layouts">
|
||||||
|
<ion-select-option class="ion-text-wrap" [value]="layout">{{ 'addon.block_myoverview.'+layout | translate }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ng-container>
|
||||||
|
</core-combobox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filter courses. -->
|
<!-- Filter courses. -->
|
||||||
<ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="courses.filter" (ionInput)="filterChanged($event)"
|
<ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="courses.filter" (ionInput)="filterChanged($event)"
|
||||||
(ionCancel)="filterChanged($event)" [placeholder]="'core.courses.filtermycourses' | translate">
|
(ionCancel)="filterChanged($event)" [placeholder]="'core.courses.filtermycourses' | translate">
|
||||||
|
@ -88,7 +97,7 @@
|
||||||
|
|
||||||
<!-- List of courses. -->
|
<!-- List of courses. -->
|
||||||
<div class="safe-area-padding">
|
<div class="safe-area-padding">
|
||||||
<ion-grid class="ion-no-padding">
|
<ion-grid class="ion-no-padding" [class.core-no-grid]="selectedLayout != 'card'">
|
||||||
<ion-row class="ion-no-padding">
|
<ion-row class="ion-no-padding">
|
||||||
<ion-col *ngFor="let course of filteredCourses" class="ion-no-padding" size="12" size-sm="6" size-md="6" size-lg="4"
|
<ion-col *ngFor="let course of filteredCourses" class="ion-no-padding" size="12" size-sm="6" size-md="6" size-lg="4"
|
||||||
size-xl="3">
|
size-xl="3">
|
||||||
|
|
|
@ -59,54 +59,54 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
value: string;
|
value: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
selectedFilter = 'inprogress';
|
timeSelectorFilter = 'inprogress';
|
||||||
sort = 'fullname';
|
sort = 'fullname';
|
||||||
currentSite?: CoreSite;
|
currentSite?: CoreSite;
|
||||||
filteredCourses: CoreEnrolledCourseDataWithOptions[] = [];
|
filteredCourses: CoreEnrolledCourseDataWithOptions[] = [];
|
||||||
prefetchCoursesData = {
|
prefetchCoursesData: Record<string, CorePrefetchStatusInfo> = {
|
||||||
all: <CorePrefetchStatusInfo> {
|
all: {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: 'core.loading',
|
statusTranslatable: 'core.loading',
|
||||||
status: '',
|
status: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
allincludinghidden: <CorePrefetchStatusInfo> {
|
allincludinghidden: {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: 'core.loading',
|
statusTranslatable: 'core.loading',
|
||||||
status: '',
|
status: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
inprogress: <CorePrefetchStatusInfo> {
|
inprogress: {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: 'core.loading',
|
statusTranslatable: 'core.loading',
|
||||||
status: '',
|
status: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
past: <CorePrefetchStatusInfo> {
|
past: {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: 'core.loading',
|
statusTranslatable: 'core.loading',
|
||||||
status: '',
|
status: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
future: <CorePrefetchStatusInfo> {
|
future: {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: 'core.loading',
|
statusTranslatable: 'core.loading',
|
||||||
status: '',
|
status: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
favourite: <CorePrefetchStatusInfo> {
|
favourite: {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: 'core.loading',
|
statusTranslatable: 'core.loading',
|
||||||
status: '',
|
status: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
hidden: <CorePrefetchStatusInfo> {
|
hidden: {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: 'core.loading',
|
statusTranslatable: 'core.loading',
|
||||||
status: '',
|
status: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
custom: <CorePrefetchStatusInfo> {
|
custom: {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: '',
|
statusTranslatable: '',
|
||||||
status: '',
|
status: '',
|
||||||
|
@ -126,12 +126,15 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
};
|
};
|
||||||
|
|
||||||
showFilter = false;
|
showFilter = false;
|
||||||
showSelectorFilter = false;
|
showTimeSelectorFilter = false;
|
||||||
showSortFilter = false;
|
showSortFilter = false;
|
||||||
downloadCourseEnabled = false;
|
downloadCourseEnabled = false;
|
||||||
downloadCoursesEnabled = false;
|
downloadCoursesEnabled = false;
|
||||||
showSortByShortName = false;
|
showSortByShortName = false;
|
||||||
|
|
||||||
|
layouts: AddonBlockMyOverviewLayouts[] = [];
|
||||||
|
selectedLayout: AddonBlockMyOverviewLayouts = 'card';
|
||||||
|
|
||||||
protected prefetchIconsInitialized = false;
|
protected prefetchIconsInitialized = false;
|
||||||
protected isDestroyed = false;
|
protected isDestroyed = false;
|
||||||
protected coursesObserver?: CoreEventObserver;
|
protected coursesObserver?: CoreEventObserver;
|
||||||
|
@ -169,21 +172,31 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
CoreSites.getCurrentSiteId(),
|
CoreSites.getCurrentSiteId(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.currentSite = CoreSites.getCurrentSite();
|
this.currentSite = CoreSites.getRequiredCurrentSite();
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
if (this.currentSite) {
|
promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewSort', this.sort).then((value) => {
|
||||||
promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewSort', this.sort).then((value) => {
|
this.sort = value;
|
||||||
this.sort = value;
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}));
|
}));
|
||||||
promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewFilter', this.selectedFilter).then((value) => {
|
promises.push(this.currentSite.getLocalSiteConfig(
|
||||||
this.selectedFilter = value;
|
'AddonBlockMyOverviewFilter',
|
||||||
|
this.timeSelectorFilter,
|
||||||
|
).then((value) => {
|
||||||
|
this.timeSelectorFilter = value;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
promises.push(this.currentSite.getLocalSiteConfig(
|
||||||
|
'AddonBlockMyOverviewLayout',
|
||||||
|
this.selectedLayout,
|
||||||
|
).then((value) => {
|
||||||
|
this.selectedLayout = value;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
|
||||||
Promise.all(promises).finally(() => {
|
Promise.all(promises).finally(() => {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
|
@ -229,6 +242,8 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
protected async fetchContent(refresh?: boolean): Promise<void> {
|
protected async fetchContent(refresh?: boolean): Promise<void> {
|
||||||
const config = this.block.configsRecord;
|
const config = this.block.configsRecord;
|
||||||
|
|
||||||
|
this.loadLayouts(config?.layouts?.value.split(','));
|
||||||
|
|
||||||
const showCategories = config?.displaycategories?.value == '1';
|
const showCategories = config?.displaycategories?.value == '1';
|
||||||
|
|
||||||
const courses = await CoreCoursesHelper.getUserCoursesWithOptions(this.sort, undefined, undefined, showCategories, {
|
const courses = await CoreCoursesHelper.getUserCoursesWithOptions(this.sort, undefined, undefined, showCategories, {
|
||||||
|
@ -280,23 +295,23 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
this.courses.future.length === 0,
|
this.courses.future.length === 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 ||
|
this.showTimeSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 ||
|
||||||
typeof courses[0].enddate != 'undefined');
|
typeof courses[0].enddate != 'undefined');
|
||||||
|
|
||||||
this.showFilters.hidden = this.getShowFilterValue(
|
this.showFilters.hidden = this.getShowFilterValue(
|
||||||
this.showSelectorFilter && typeof courses[0].hidden != 'undefined' &&
|
this.showTimeSelectorFilter && typeof courses[0].hidden != 'undefined' &&
|
||||||
(!config || config.displaygroupinghidden?.value == '1'),
|
(!config || config.displaygroupinghidden?.value == '1'),
|
||||||
this.courses.hidden.length === 0,
|
this.courses.hidden.length === 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.showFilters.favourite = this.getShowFilterValue(
|
this.showFilters.favourite = this.getShowFilterValue(
|
||||||
this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined' &&
|
this.showTimeSelectorFilter && typeof courses[0].isfavourite != 'undefined' &&
|
||||||
(!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1'),
|
(!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1'),
|
||||||
this.courses.favourite.length === 0,
|
this.courses.favourite.length === 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.showFilters.custom = this.getShowFilterValue(
|
this.showFilters.custom = this.getShowFilterValue(
|
||||||
this.showSelectorFilter && config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value,
|
this.showTimeSelectorFilter && config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
if (this.showFilters.custom == 'show') {
|
if (this.showFilters.custom == 'show') {
|
||||||
|
@ -305,25 +320,54 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
this.customFilter = [];
|
this.customFilter = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.showSelectorFilter) {
|
if (this.showTimeSelectorFilter) {
|
||||||
// Check if any selector is shown and not disabled.
|
// Check if any selector is shown and not disabled.
|
||||||
this.showSelectorFilter = Object.keys(this.showFilters).some((key) => this.showFilters[key] == 'show');
|
this.showTimeSelectorFilter = Object.keys(this.showFilters).some((key) => this.showFilters[key] == 'show');
|
||||||
|
|
||||||
if (!this.showSelectorFilter) {
|
if (!this.showTimeSelectorFilter) {
|
||||||
// All filters disabled, display all the courses.
|
// All filters disabled, display all the courses.
|
||||||
this.showFilters.all = 'show';
|
this.showFilters.all = 'show';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.showSelectorFilter) {
|
if (!this.showTimeSelectorFilter) {
|
||||||
// No selector, display all the courses.
|
// No selector, display all the courses.
|
||||||
this.selectedFilter = 'all';
|
this.timeSelectorFilter = 'all';
|
||||||
}
|
}
|
||||||
this.setCourseFilter(this.selectedFilter);
|
this.setCourseFilter(this.timeSelectorFilter);
|
||||||
|
|
||||||
this.initPrefetchCoursesIcons();
|
this.initPrefetchCoursesIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load block layouts.
|
||||||
|
*
|
||||||
|
* @param layouts Config available layouts.
|
||||||
|
*/
|
||||||
|
protected loadLayouts(layouts: string[] = []): void {
|
||||||
|
this.layouts = [];
|
||||||
|
|
||||||
|
layouts.forEach((layout) => {
|
||||||
|
if (layout == '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validLayout: AddonBlockMyOverviewLayouts = layout == 'summary' ? 'list' : layout as AddonBlockMyOverviewLayouts;
|
||||||
|
if (!this.layouts.includes(validLayout)) {
|
||||||
|
this.layouts.push(validLayout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no layout is available use card.
|
||||||
|
if (this.layouts.length == 0) {
|
||||||
|
this.layouts = ['card'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.layouts.includes(this.selectedLayout)) {
|
||||||
|
this.selectedLayout = this.layouts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to help with filter values.
|
* Helper function to help with filter values.
|
||||||
*
|
*
|
||||||
|
@ -417,7 +461,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async prefetchCourses(): Promise<void> {
|
async prefetchCourses(): Promise<void> {
|
||||||
const selected = this.selectedFilter;
|
const selected = this.timeSelectorFilter;
|
||||||
const initialIcon = this.prefetchCoursesData[selected].icon;
|
const initialIcon = this.prefetchCoursesData[selected].icon;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -442,13 +486,14 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The selected courses filter have changed.
|
* The layout have changed.
|
||||||
*
|
*
|
||||||
* @param filter New filter
|
* @param layout New layout.
|
||||||
*/
|
*/
|
||||||
selectedChanged(filter: string): void {
|
layoutChanged(layout: AddonBlockMyOverviewLayouts): void {
|
||||||
this.selectedFilter = filter;
|
this.selectedLayout = layout;
|
||||||
this.setCourseFilter(this.selectedFilter);
|
|
||||||
|
this.currentSite?.setLocalSiteConfig('AddonBlockMyOverviewLayout', layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -457,7 +502,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
* @param filter Filter name to set.
|
* @param filter Filter name to set.
|
||||||
*/
|
*/
|
||||||
protected async setCourseFilter(filter: string): Promise<void> {
|
protected async setCourseFilter(filter: string): Promise<void> {
|
||||||
this.selectedFilter = filter;
|
this.timeSelectorFilter = filter;
|
||||||
|
|
||||||
if (this.showFilters.custom == 'show' && filter.startsWith('custom-') &&
|
if (this.showFilters.custom == 'show' && filter.startsWith('custom-') &&
|
||||||
typeof this.customFilter[filter.substr(7)] != 'undefined') {
|
typeof this.customFilter[filter.substr(7)] != 'undefined') {
|
||||||
|
@ -497,6 +542,16 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time selector courses filter have changed.
|
||||||
|
*
|
||||||
|
* @param filter New filter
|
||||||
|
*/
|
||||||
|
timeSelectorChanged(filter: string): void {
|
||||||
|
this.timeSelectorFilter = filter;
|
||||||
|
this.setCourseFilter(this.timeSelectorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init courses filters.
|
* Init courses filters.
|
||||||
*
|
*
|
||||||
|
@ -556,7 +611,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setCourseFilter(this.selectedFilter);
|
this.setCourseFilter(this.timeSelectorFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -580,7 +635,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
if (this.showFilter) {
|
if (this.showFilter) {
|
||||||
this.filteredCourses = this.courses.allincludinghidden;
|
this.filteredCourses = this.courses.allincludinghidden;
|
||||||
} else {
|
} else {
|
||||||
this.setCourseFilter(this.selectedFilter);
|
this.setCourseFilter(this.timeSelectorFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -614,3 +669,5 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddonBlockMyOverviewLayouts = 'card'|'list';
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
{
|
{
|
||||||
"all": "All (except removed from view)",
|
"all": "All (except removed from view)",
|
||||||
"allincludinghidden": "All",
|
"allincludinghidden": "All",
|
||||||
|
"card": "Card",
|
||||||
"favourites": "Starred",
|
"favourites": "Starred",
|
||||||
"future": "Future",
|
"future": "Future",
|
||||||
"hiddencourses": "Removed from view",
|
"hiddencourses": "Removed from view",
|
||||||
"inprogress": "In progress",
|
"inprogress": "In progress",
|
||||||
"lastaccessed": "Last accessed",
|
"lastaccessed": "Last accessed",
|
||||||
|
"list": "List",
|
||||||
"nocourses": "No courses",
|
"nocourses": "No courses",
|
||||||
"past": "Past",
|
"past": "Past",
|
||||||
"pluginname": "Course overview",
|
"pluginname": "Course overview",
|
||||||
|
|
Loading…
Reference in New Issue