commit
cbfc866af4
|
@ -1,5 +1,11 @@
|
|||
module.exports = {
|
||||
framework: '@storybook/angular',
|
||||
addons: ['@storybook/addon-controls'],
|
||||
addons: [
|
||||
'@storybook/addon-controls',
|
||||
'@storybook/addon-viewport',
|
||||
'storybook-addon-designs',
|
||||
'storybook-addon-rtl-direction',
|
||||
'storybook-dark-mode',
|
||||
],
|
||||
stories: ['../src/**/*.stories.ts'],
|
||||
}
|
||||
|
|
|
@ -3,4 +3,9 @@ import '!style-loader!css-loader!sass-loader!./styles.scss';
|
|||
|
||||
export const parameters = {
|
||||
layout: 'centered',
|
||||
darkMode: {
|
||||
darkClass: 'dark',
|
||||
classTarget: 'html',
|
||||
stylePreview: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
storybook-dynamic-app-root {
|
||||
color: var(--ion-text-color);
|
||||
}
|
||||
|
||||
.core-error-info {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"",
|
||||
"@Component({",
|
||||
" selector: '$2${TM_FILENAME_BASE}',",
|
||||
" templateUrl: '$2${TM_FILENAME_BASE}.html',",
|
||||
" templateUrl: '${TM_FILENAME_BASE}.html',",
|
||||
"})",
|
||||
"export class ${1:${TM_FILENAME_BASE}}Component {",
|
||||
"",
|
||||
|
@ -110,6 +110,24 @@
|
|||
],
|
||||
"description": "[Moodle] Create a Pure Singleton"
|
||||
},
|
||||
"[Moodle] Events": {
|
||||
"prefix": "maeventsdeclaration",
|
||||
"body": [
|
||||
"declare module '@singletons/events' {",
|
||||
"",
|
||||
" /**",
|
||||
" * Augment CoreEventsData interface with events specific to this service.",
|
||||
" *",
|
||||
" * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation",
|
||||
" */",
|
||||
" export interface CoreEventsData {",
|
||||
" [$1]: $2;",
|
||||
" }",
|
||||
"",
|
||||
"}"
|
||||
],
|
||||
"description": ""
|
||||
},
|
||||
"Innherit doc": {
|
||||
"prefix": "inheritdoc",
|
||||
"body": [
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -149,9 +149,11 @@
|
|||
"@ionic/angular-toolkit": "^2.3.3",
|
||||
"@ionic/cli": "^6.19.0",
|
||||
"@storybook/addon-controls": "~6.1.21",
|
||||
"@storybook/addon-viewport": "~6.1.21",
|
||||
"@storybook/angular": "~6.1.21",
|
||||
"@types/faker": "^5.1.3",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/marked": "^4.3.1",
|
||||
"@types/node": "^12.12.64",
|
||||
"@types/resize-observer-browser": "^0.1.5",
|
||||
"@types/webpack-env": "^1.16.0",
|
||||
|
@ -183,9 +185,13 @@
|
|||
"jest-preset-angular": "^8.3.1",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"jsonc-parser": "^2.3.1",
|
||||
"marked": "^4.3.0",
|
||||
"minimatch": "^5.1.0",
|
||||
"native-run": "^1.4.0",
|
||||
"patch-package": "^6.5.0",
|
||||
"storybook-addon-designs": "~6.1.0",
|
||||
"storybook-addon-rtl-direction": "0.0.19",
|
||||
"storybook-dark-mode": "^3.0.0",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-jest": "^26.4.1",
|
||||
"ts-node": "~8.3.0",
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"addon.block_calendarupcoming.pluginname": "block_calendar_upcoming",
|
||||
"addon.block_comments.pluginname": "block_comments",
|
||||
"addon.block_completionstatus.pluginname": "block_completionstatus",
|
||||
"addon.block_globalsearch.pluginname": "block_globalsearch",
|
||||
"addon.block_glossaryrandom.pluginname": "block_glossary_random",
|
||||
"addon.block_learningplans.pluginname": "block_lp",
|
||||
"addon.block_myoverview.all": "block_myoverview",
|
||||
|
@ -2310,6 +2311,16 @@
|
|||
"core.scanqr": "local_moodlemobileapp",
|
||||
"core.scrollbackward": "local_moodlemobileapp",
|
||||
"core.scrollforward": "local_moodlemobileapp",
|
||||
"core.search.allcourses": "search",
|
||||
"core.search.allcategories": "local_moodlemobileapp",
|
||||
"core.search.empty": "local_moodlemobileapp",
|
||||
"core.search.filtercategories": "local_moodlemobileapp",
|
||||
"core.search.filtercourses": "local_moodlemobileapp",
|
||||
"core.search.filterheader": "search",
|
||||
"core.search.globalsearch": "search",
|
||||
"core.search.noresults": "local_moodlemobileapp",
|
||||
"core.search.noresultshelp": "local_moodlemobileapp",
|
||||
"core.search.resultby": "local_moodlemobileapp",
|
||||
"core.search": "moodle",
|
||||
"core.searching": "local_moodlemobileapp",
|
||||
"core.searchresults": "moodle",
|
||||
|
|
|
@ -41,6 +41,7 @@ import { AddonBlockSiteMainMenuModule } from './sitemainmenu/sitemainmenu.module
|
|||
import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses.module';
|
||||
import { AddonBlockTagsModule } from './tags/tags.module';
|
||||
import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
||||
import { AddonBlockGlobalSearchModule } from '@addons/block/globalsearch/globalsearch.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -55,6 +56,7 @@ import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
|||
AddonBlockCommentsModule,
|
||||
AddonBlockCompletionStatusModule,
|
||||
AddonBlockCourseListModule,
|
||||
AddonBlockGlobalSearchModule,
|
||||
AddonBlockGlossaryRandomModule,
|
||||
AddonBlockHtmlModule,
|
||||
AddonBlockLearningPlansModule,
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreBlockDelegate } from '@features/block/services/block-delegate';
|
||||
import { AddonBlockGlobalSearchHandler } from './services/block-handler';
|
||||
import { CoreBlockComponentsModule } from '@features/block/components/components.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
IonicModule,
|
||||
CoreBlockComponentsModule,
|
||||
TranslateModule.forChild(),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
CoreBlockDelegate.registerHandler(AddonBlockGlobalSearchHandler.instance);
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AddonBlockGlobalSearchModule {}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "Global search"
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
|
||||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
|
||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreCourseBlock } from '@features/course/services/course';
|
||||
import { CORE_SEARCH_PAGE_NAME } from '@features/search/services/handlers/mainmenu';
|
||||
import { CoreSearchGlobalSearch } from '@features/search/services/global-search';
|
||||
|
||||
/**
|
||||
* Block handler.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonBlockGlobalSearchHandlerService extends CoreBlockBaseHandler {
|
||||
|
||||
name = 'AddonBlockGlobalSearch';
|
||||
blockName = 'globalsearch';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return CoreSearchGlobalSearch.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData | undefined {
|
||||
return {
|
||||
title: 'addon.block_globalsearch.pluginname',
|
||||
class: 'addon-block-globalsearch',
|
||||
component: CoreBlockOnlyTitleComponent,
|
||||
link: CORE_SEARCH_PAGE_NAME,
|
||||
linkParams: contextLevel === 'course'
|
||||
? { courseId: instanceId }
|
||||
: {},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonBlockGlobalSearchHandler = makeSingleton(AddonBlockGlobalSearchHandlerService);
|
|
@ -21,7 +21,6 @@ import {
|
|||
} from '../../services/recentlyaccesseditems';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
/**
|
||||
|
@ -88,10 +87,7 @@ export class AddonBlockRecentlyAccessedItemsComponent extends CoreBlockBaseCompo
|
|||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
const treated = await CoreContentLinksHelper.handleLink(url);
|
||||
if (!treated) {
|
||||
return CoreSites.getCurrentSite()?.openInBrowserWithAutoLogin(url);
|
||||
}
|
||||
await CoreSites.visitLink(url);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||
import { AddonBlockTimelineDayEvents } from '@addons/block/timeline/classes/section';
|
||||
|
||||
|
@ -54,10 +53,7 @@ export class AddonBlockTimelineEventsComponent {
|
|||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
const treated = await CoreContentLinksHelper.handleLink(url);
|
||||
if (!treated) {
|
||||
return CoreSites.getRequiredCurrentSite().openInBrowserWithAutoLogin(url);
|
||||
}
|
||||
await CoreSites.visitLink(url);
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
import { CoreCourse, CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
|
@ -428,11 +427,7 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave {
|
|||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
const treated = await CoreContentLinksHelper.handleLink(this.siteAfterSubmit);
|
||||
|
||||
if (!treated) {
|
||||
await this.currentSite.openInBrowserWithAutoLogin(this.siteAfterSubmit);
|
||||
}
|
||||
await CoreSites.visitLink(this.siteAfterSubmit, { siteId: this.currentSite.id });
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import { CoreEvents } from '@singletons/events';
|
|||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||
import { CoreConstants, ModPurpose } from '@/core/constants';
|
||||
import { AddonModForumIndexComponent } from '../../components/index';
|
||||
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
|
||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||
import { CoreIonicColorNames } from '@singletons/colors';
|
||||
|
@ -86,6 +85,8 @@ export class AddonModForumModuleHandlerService extends CoreModuleHandlerBase imp
|
|||
* @inheritdoc
|
||||
*/
|
||||
async getMainComponent(): Promise<Type<unknown> | undefined> {
|
||||
const { AddonModForumIndexComponent } = await import('../../components/index');
|
||||
|
||||
return AddonModForumIndexComponent;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
@ -33,11 +32,10 @@ export class AddonModUrlHelperProvider {
|
|||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
const treated = await CoreContentLinksHelper.handleLink(url, undefined, true, true);
|
||||
|
||||
if (!treated) {
|
||||
await CoreSites.getCurrentSite()?.openInBrowserWithAutoLogin(url);
|
||||
}
|
||||
await CoreSites.visitLink(url, {
|
||||
checkRoot: true,
|
||||
openBrowserRoot: true,
|
||||
});
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import { Injectable } from '@angular/core';
|
|||
|
||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
|
@ -75,13 +74,7 @@ export class AddonReportInsightsActionLinkHandlerService extends CoreContentLink
|
|||
// Try to open the link in the app.
|
||||
const forwardUrl = decodeURIComponent(params.forwardurl);
|
||||
|
||||
const treated = await CoreContentLinksHelper.handleLink(forwardUrl);
|
||||
if (!treated) {
|
||||
// Cannot be opened in the app, open in browser.
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
await site.openInBrowserWithAutoLogin(forwardUrl);
|
||||
}
|
||||
await CoreSites.visitLink(forwardUrl, { siteId });
|
||||
}
|
||||
},
|
||||
}];
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[{"id":1,"courseimage":"https://picsum.photos/500/500","shortname":"Moodle and Mountaineering","summary":"This course will introduce you to the basics of Alpine Mountaineering, while at the same time highlighting some of the great features of Moodle."},{"id":2,"courseimage":"assets/storybook/geopattern.svg","shortname":"Digital Literacy","summary":"This course explores Digital Literacy and its importance for teachers and students. The course is optimised for the Moodle App. Please try it out!"},{"id":3,"shortname":"Class and Conflict in World Cinema","summary":"In this module we will analyse two very significant films - City of God and La Haine, both of which depict violent lives in poor conditions, the former in the favelas of Brazil and the latter in a Parisian banlieue. We will look at how conflict and class are portrayed, focusing particularly on the use of mise en scène."}]
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1 @@
|
|||
{"id":"123456","info":{"version":"2022041900","sitename":"School","username":"barbara","firstname":"Barbara","lastname":"Gardner","fullname":"Barbara Gardner","lang":"en","userid":1,"siteurl":"https://campus.example.edu","userpictureurl":"","functions":[]}}
|
|
@ -36,6 +36,15 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
this.loadedPromise = new Promise(resolve => this.resolveLoaded = resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the source is dirty.
|
||||
*
|
||||
* @returns Whether the source is dirty.
|
||||
*/
|
||||
isDirty(): boolean {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether data is loaded.
|
||||
*
|
||||
|
@ -88,6 +97,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
reset(): void {
|
||||
this.items = null;
|
||||
this.dirty = false;
|
||||
this.loaded = false;
|
||||
|
||||
this.listeners.forEach(listener => listener.onReset?.call(listener));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreItemsManagerSource } from './items-manager-source';
|
||||
|
||||
/**
|
||||
* Paginated items collection source data.
|
||||
*/
|
||||
export abstract class CorePaginatedItemsManagerSource<Item = unknown> extends CoreItemsManagerSource<Item> {
|
||||
|
||||
protected hasMoreItems = true;
|
||||
|
||||
/**
|
||||
* Check whether there are more pages to be loaded.
|
||||
*
|
||||
* @returns Whether there are more pages to be loaded.
|
||||
*/
|
||||
isCompleted(): boolean {
|
||||
return !this.hasMoreItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the source is empty or not.
|
||||
*
|
||||
* @returns Whether the source is empty.
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return !this.isLoaded() || (this.getItems() ?? []).length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of pages that have been loaded.
|
||||
*
|
||||
* @returns Pages loaded.
|
||||
*/
|
||||
getPagesLoaded(): number {
|
||||
if (this.items === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const pageLength = this.getPageLength();
|
||||
if (pageLength === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.ceil(this.items.length / pageLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset collection data.
|
||||
*/
|
||||
reset(): void {
|
||||
this.hasMoreItems = true;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the collection, this resets the data to the first page.
|
||||
*/
|
||||
async reload(): Promise<void> {
|
||||
this.dirty = true;
|
||||
|
||||
await this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more items, if any.
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
if (this.dirty) {
|
||||
const { items, hasMoreItems } = await this.loadPageItems(0);
|
||||
|
||||
this.dirty = false;
|
||||
this.setItems(items, hasMoreItems ?? false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hasMoreItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { items, hasMoreItems } = await this.loadPageItems(this.getPagesLoaded());
|
||||
|
||||
this.setItems((this.items ?? []).concat(items), hasMoreItems ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load page items.
|
||||
*
|
||||
* @param page Page number (starting at 0).
|
||||
* @returns Page items data.
|
||||
*/
|
||||
protected abstract loadPageItems(page: number): Promise<{ items: Item[]; hasMoreItems?: boolean }>;
|
||||
|
||||
/**
|
||||
* Get the length of each page in the collection.
|
||||
*
|
||||
* @returns Page length; null for collections that don't support pagination.
|
||||
*/
|
||||
protected getPageLength(): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the collection items.
|
||||
*
|
||||
* @param items Items.
|
||||
* @param hasMoreItems Whether there are more pages to be loaded.
|
||||
*/
|
||||
protected setItems(items: Item[], hasMoreItems = false): void {
|
||||
this.hasMoreItems = hasMoreItems;
|
||||
|
||||
super.setItems(items);
|
||||
}
|
||||
|
||||
}
|
|
@ -13,14 +13,12 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreItemsManagerSource } from './items-manager-source';
|
||||
import { CorePaginatedItemsManagerSource } from './paginated-items-manager-source';
|
||||
|
||||
/**
|
||||
* Routed items collection source data.
|
||||
*/
|
||||
export abstract class CoreRoutedItemsManagerSource<Item = unknown> extends CoreItemsManagerSource<Item> {
|
||||
|
||||
protected hasMoreItems = true;
|
||||
export abstract class CoreRoutedItemsManagerSource<Item = unknown> extends CorePaginatedItemsManagerSource<Item> {
|
||||
|
||||
/**
|
||||
* Get a string to identify instances constructed with the given arguments as being reusable.
|
||||
|
@ -32,102 +30,6 @@ export abstract class CoreRoutedItemsManagerSource<Item = unknown> extends CoreI
|
|||
return args.map(argument => String(argument)).join('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there are more pages to be loaded.
|
||||
*
|
||||
* @returns Whether there are more pages to be loaded.
|
||||
*/
|
||||
isCompleted(): boolean {
|
||||
return !this.hasMoreItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of pages that have been loaded.
|
||||
*
|
||||
* @returns Pages loaded.
|
||||
*/
|
||||
getPagesLoaded(): number {
|
||||
if (this.items === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const pageLength = this.getPageLength();
|
||||
if (pageLength === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.ceil(this.items.length / pageLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset collection data.
|
||||
*/
|
||||
reset(): void {
|
||||
this.hasMoreItems = true;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the collection, this resets the data to the first page.
|
||||
*/
|
||||
async reload(): Promise<void> {
|
||||
this.dirty = true;
|
||||
|
||||
await this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more items, if any.
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
if (this.dirty) {
|
||||
const { items, hasMoreItems } = await this.loadPageItems(0);
|
||||
|
||||
this.dirty = false;
|
||||
this.setItems(items, hasMoreItems ?? false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hasMoreItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { items, hasMoreItems } = await this.loadPageItems(this.getPagesLoaded());
|
||||
|
||||
this.setItems((this.items ?? []).concat(items), hasMoreItems ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load page items.
|
||||
*
|
||||
* @param page Page number (starting at 0).
|
||||
* @returns Page items data.
|
||||
*/
|
||||
protected abstract loadPageItems(page: number): Promise<{ items: Item[]; hasMoreItems?: boolean }>;
|
||||
|
||||
/**
|
||||
* Get the length of each page in the collection.
|
||||
*
|
||||
* @returns Page length; null for collections that don't support pagination.
|
||||
*/
|
||||
protected getPageLength(): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the collection items.
|
||||
*
|
||||
* @param items Items.
|
||||
* @param hasMoreItems Whether there are more pages to be loaded.
|
||||
*/
|
||||
protected setItems(items: Item[], hasMoreItems = false): void {
|
||||
this.hasMoreItems = hasMoreItems;
|
||||
|
||||
super.setItems(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query parameters to use when navigating to an item page.
|
||||
*
|
||||
|
|
|
@ -2747,6 +2747,8 @@ export const enum CoreSiteConfigSupportAvailability {
|
|||
*/
|
||||
export type CoreSiteConfig = Record<string, string> & {
|
||||
supportavailability?: string; // String representation of CoreSiteConfigSupportAvailability.
|
||||
searchbanner?: string; // Search banner text.
|
||||
searchbannerenable?: string; // Whether search banner is enabled.
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -64,6 +64,7 @@ import { CoreMessageComponent } from './message/message';
|
|||
import { CoreGroupSelectorComponent } from './group-selector/group-selector';
|
||||
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
|
||||
import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
||||
import { CoreCourseImageComponent } from '@components/course-image/course-image';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -75,6 +76,7 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
|||
CoreContextMenuComponent,
|
||||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreCourseImageComponent,
|
||||
CoreDownloadRefreshComponent,
|
||||
CoreDynamicComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
|
@ -128,6 +130,7 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
|||
CoreContextMenuComponent,
|
||||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreCourseImageComponent,
|
||||
CoreDownloadRefreshComponent,
|
||||
CoreDynamicComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<ion-icon *ngIf="!course.courseimage" name="fas-graduation-cap" slot="start" aria-hidden="true">
|
||||
</ion-icon>
|
||||
<ion-avatar *ngIf="course.courseimage" slot="start">
|
||||
<img [src]="course.courseimage" core-external-content alt="" (error)="loadFallbackCourseIcon()" />
|
||||
</ion-avatar>
|
|
@ -0,0 +1,65 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
--core-image-radius: var(--core-courseimage-radius);
|
||||
--core-image-size: 60px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--course-color, white);
|
||||
border-radius: var(--core-image-radius);
|
||||
|
||||
@for $i from 0 to length($core-course-image-background) {
|
||||
&.course-color-#{$i} {
|
||||
--course-color: var(--core-course-color-#{$i});
|
||||
--course-color-tint: var(--core-course-color-#{$i}-tint);
|
||||
}
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
--padding: 12px;
|
||||
|
||||
padding: var(--padding);
|
||||
font-size: calc(var(--core-image-size) - var(--padding) * 2);
|
||||
color: var(--course-color-tint);
|
||||
}
|
||||
|
||||
ion-avatar {
|
||||
--border-radius: var(--core-image-radius);
|
||||
width: var(--core-image-size);
|
||||
height: var(--core-image-size);
|
||||
|
||||
img {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
img[src$=".svg"] {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
&.fill-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
--core-image-radius: 0px;
|
||||
--core-image-size: 100%;
|
||||
|
||||
ion-icon {
|
||||
opacity: 0.5;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
:host-context(ion-item) {
|
||||
@include margin(6px, 8px, 6px, 0px);
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, ElementRef, OnInit, OnChanges, HostBinding } from '@angular/core';
|
||||
import { CoreCourseListItem } from '@features/courses/services/courses';
|
||||
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
|
||||
import { CoreColors } from '@singletons/colors';
|
||||
|
||||
@Component({
|
||||
selector: 'core-course-image',
|
||||
templateUrl: 'course-image.html',
|
||||
styleUrls: ['./course-image.scss'],
|
||||
})
|
||||
export class CoreCourseImageComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input() course!: CoreCourseListItem;
|
||||
@Input() fill = false;
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(element: ElementRef) {
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
@HostBinding('class.fill-container')
|
||||
get fillContainer(): boolean {
|
||||
return this.fill;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.setCourseColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(): void {
|
||||
this.setCourseColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the course image set because it cannot be loaded and set the fallback icon color.
|
||||
*/
|
||||
loadFallbackCourseIcon(): void {
|
||||
this.course.courseimage = undefined;
|
||||
|
||||
// Set the color because it won't be set at this point.
|
||||
this.setCourseColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set course color.
|
||||
*/
|
||||
protected async setCourseColor(): Promise<void> {
|
||||
await CoreCoursesHelper.loadCourseColorAndImage(this.course);
|
||||
|
||||
if (this.course.color) {
|
||||
this.element.style.setProperty('--course-color', this.course.color);
|
||||
|
||||
const tint = CoreColors.lighter(this.course.color, 50);
|
||||
this.element.style.setProperty('--course-color-tint', tint);
|
||||
} else if(this.course.colorNumber !== undefined) {
|
||||
this.element.classList.add('course-color-' + this.course.colorNumber);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +1,23 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
--image-size: 120px;
|
||||
--icon-color: var(--text-color);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
|
||||
color: var(--text-color);
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
--image-size: 120px;
|
||||
|
||||
height: 100%;
|
||||
|
||||
ion-icon {
|
||||
font-size: var(--image-size);
|
||||
color: var(--icon-color);
|
||||
}
|
||||
img {
|
||||
height: var(--image-size);
|
||||
|
@ -28,6 +29,20 @@
|
|||
&.core-empty-box-clickable {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&.dimmed {
|
||||
--icon-color: var(--gray-400);
|
||||
--text-color: var(--gray-700);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
:host-context(html.dark) {
|
||||
|
||||
&.dimmed {
|
||||
--text-color: var(--gray-300);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, HostBinding, Input } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Component to show an empty box message. It will show an optional icon or image and a text centered on page.
|
||||
|
@ -30,6 +30,7 @@ import { Component, Input } from '@angular/core';
|
|||
export class CoreEmptyBoxComponent {
|
||||
|
||||
@Input() message = ''; // Message to display.
|
||||
@Input() dimmed = false; // Wether the box is dimmed or not.
|
||||
@Input() icon?: string; // Name of the icon to use.
|
||||
@Input() image?: string; // Image source. If an icon is provided, image won't be used.
|
||||
@Input() flipIconRtl = false; // Whether to flip the icon in RTL. Defaults to false.
|
||||
|
@ -39,4 +40,9 @@ export class CoreEmptyBoxComponent {
|
|||
*/
|
||||
@Input() inline = false;
|
||||
|
||||
@HostBinding('class.dimmed')
|
||||
get isDimmed(): boolean {
|
||||
return this.dimmed;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CoreEmptyBoxPageComponent } from './empty-box-page/empty-box-page';
|
||||
import { CoreEmptyBoxWrapperComponent } from './empty-box-wrapper/empty-box-wrapper';
|
||||
import { StorybookModule } from '@/storybook/storybook.module';
|
||||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CoreCourseImageCardsPageComponent } from '@components/stories/components/course-image-cards-page/course-image-cards-page';
|
||||
import { CoreCourseImageListPageComponent } from '@components/stories/components/course-image-list-page/course-image-list-page';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCourseImageCardsPageComponent,
|
||||
CoreCourseImageListPageComponent,
|
||||
CoreEmptyBoxPageComponent,
|
||||
CoreEmptyBoxWrapperComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
StorybookModule,
|
||||
CoreComponentsModule,
|
||||
CoreSearchComponentsModule,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsStorybookModule {}
|
|
@ -0,0 +1,22 @@
|
|||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h1>Course Cards</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-card *ngFor="let course of courses">
|
||||
<div class="course-image-wrapper">
|
||||
<core-course-image [course]="course" [fill]="true"></core-course-image>
|
||||
</div>
|
||||
<ion-card-header>
|
||||
<ion-card-title>{{ course.shortname }}</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
{{ course.summary }}
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-content>
|
||||
</ion-app>
|
|
@ -0,0 +1,16 @@
|
|||
:host {
|
||||
|
||||
ion-card {
|
||||
max-width: 350px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.course-image-wrapper {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-top: 40%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { CoreCourseListItem } from '@features/courses/services/courses';
|
||||
import courses from '@/assets/storybook/courses.json';
|
||||
|
||||
@Component({
|
||||
selector: 'core-course-image-cards-page',
|
||||
templateUrl: 'course-image-cards-page.html',
|
||||
styleUrls: ['./course-image-cards-page.scss'],
|
||||
})
|
||||
export class CoreCourseImageCardsPageComponent {
|
||||
|
||||
courses: Partial<CoreCourseListItem>[] = courses;
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h1>Courses List</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let course of courses">
|
||||
<core-course-image [course]="course" slot="start"></core-course-image>
|
||||
<ion-label>
|
||||
{{ course.shortname }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-app>
|
|
@ -0,0 +1,27 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { CoreCourseListItem } from '@features/courses/services/courses';
|
||||
import courses from '@/assets/storybook/courses.json';
|
||||
|
||||
@Component({
|
||||
selector: 'core-course-image-list-page',
|
||||
templateUrl: 'course-image-list-page.html',
|
||||
})
|
||||
export class CoreCourseImageListPageComponent {
|
||||
|
||||
courses: Partial<CoreCourseListItem>[] = courses;
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h1>Search</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<div class="core-flex-fill">
|
||||
<core-search-box></core-search-box>
|
||||
<core-empty-box-wrapper [icon]="icon" [content]="content" [dimmed]="dimmed" class="core-flex-fill">
|
||||
</core-empty-box-wrapper>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
|
@ -0,0 +1,27 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'core-empty-box-page',
|
||||
templateUrl: 'empty-box-page.html',
|
||||
})
|
||||
export class CoreEmptyBoxPageComponent {
|
||||
|
||||
@Input() icon!: string;
|
||||
@Input() content!: string;
|
||||
@Input() dimmed!: boolean;
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<core-empty-box [icon]="icon" [dimmed]="dimmed">
|
||||
<div [innerHTML]="html"></div>
|
||||
</core-empty-box>
|
|
@ -0,0 +1,38 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnChanges } from '@angular/core';
|
||||
import { DomSanitizer } from '@singletons';
|
||||
import { SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'core-empty-box-wrapper',
|
||||
templateUrl: 'empty-box-wrapper.html',
|
||||
})
|
||||
export class CoreEmptyBoxWrapperComponent implements OnChanges {
|
||||
|
||||
@Input() icon!: string;
|
||||
@Input() content!: string;
|
||||
@Input() dimmed!: boolean;
|
||||
|
||||
html?: SafeHtml;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(): void {
|
||||
this.html = DomSanitizer.bypassSecurityTrustHtml(this.content);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Meta, moduleMetadata } from '@storybook/angular';
|
||||
|
||||
import { story } from '@/storybook/utils/helpers';
|
||||
|
||||
import { CoreCourseImageComponent } from '@components/course-image/course-image';
|
||||
import { APP_INITIALIZER } from '@angular/core';
|
||||
import { CoreSitesStub } from '@/storybook/stubs/services/sites';
|
||||
import { CoreCourseImageListPageComponent } from '@components/stories/components/course-image-list-page/course-image-list-page';
|
||||
import { CoreComponentsStorybookModule } from '@components/stories/components/components.module';
|
||||
import { CoreCourseImageCardsPageComponent } from '@components/stories/components/course-image-cards-page/course-image-cards-page';
|
||||
|
||||
interface Args {
|
||||
type: 'image' | 'geopattern' | 'color';
|
||||
fill: boolean;
|
||||
}
|
||||
|
||||
export default <Meta> {
|
||||
title: 'Core/Course Image',
|
||||
component: CoreCourseImageComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [CoreComponentsStorybookModule],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
const site = CoreSitesStub.getRequiredCurrentSite();
|
||||
|
||||
site.stubWSResponse('tool_mobile_get_config', {
|
||||
settings: [
|
||||
{ name: 'core_admin_coursecolor1', value: '#F9B000' },
|
||||
{ name: 'core_admin_coursecolor2', value: '#EF4B00' },
|
||||
{ name: 'core_admin_coursecolor3', value: '#4338FB' },
|
||||
{ name: 'core_admin_coursecolor4', value: '#E142FB' },
|
||||
{ name: 'core_admin_coursecolor5', value: '#FF0064' },
|
||||
{ name: 'core_admin_coursecolor6', value: '#FF0F18' },
|
||||
{ name: 'core_admin_coursecolor7', value: '#039B06' },
|
||||
{ name: 'core_admin_coursecolor8', value: '#039B88' },
|
||||
{ name: 'core_admin_coursecolor9', value: '#EF009B' },
|
||||
{ name: 'core_admin_coursecolor10', value: '#020B6E' },
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['image', 'geopattern', 'color'],
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
type: 'image',
|
||||
fill: false,
|
||||
},
|
||||
};
|
||||
|
||||
const Template = story<Args>(({ type, ...args }) => {
|
||||
const getImageSource = () => {
|
||||
switch (type) {
|
||||
case 'image':
|
||||
return 'https://picsum.photos/500/500';
|
||||
case 'geopattern':
|
||||
return 'assets/storybook/geopattern.svg';
|
||||
case 'color':
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
component: CoreCourseImageComponent,
|
||||
props: {
|
||||
...args,
|
||||
course: {
|
||||
id: 1,
|
||||
courseimage: getImageSource(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const Primary = story(Template);
|
||||
export const ListPage = story(() => ({ component: CoreCourseImageListPageComponent }));
|
||||
export const CardsPage = story(() => ({ component: CoreCourseImageCardsPageComponent }));
|
|
@ -0,0 +1,79 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Meta, moduleMetadata } from '@storybook/angular';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { story } from '@/storybook/utils/helpers';
|
||||
|
||||
import { CoreEmptyBoxComponent } from '@components/empty-box/empty-box';
|
||||
import { CoreEmptyBoxWrapperComponent } from './components/empty-box-wrapper/empty-box-wrapper';
|
||||
import { CoreEmptyBoxPageComponent } from './components/empty-box-page/empty-box-page';
|
||||
import { CoreComponentsStorybookModule } from './components/components.module';
|
||||
|
||||
interface Args {
|
||||
icon: string;
|
||||
content: string;
|
||||
dimmed: boolean;
|
||||
}
|
||||
|
||||
export default <Meta<Args>> {
|
||||
title: 'Core/Empty Box',
|
||||
component: CoreEmptyBoxComponent,
|
||||
decorators: [
|
||||
moduleMetadata({ imports: [CoreComponentsStorybookModule] }),
|
||||
],
|
||||
argTypes: {
|
||||
icon: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['fas-magnifying-glass', 'fas-user', 'fas-check'],
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
icon: 'fas-user',
|
||||
content: 'No users',
|
||||
dimmed: false,
|
||||
},
|
||||
};
|
||||
|
||||
const WrapperTemplate = story<Args>((args) => ({
|
||||
component: CoreEmptyBoxWrapperComponent,
|
||||
props: {
|
||||
...args,
|
||||
content: marked(args.content),
|
||||
},
|
||||
}));
|
||||
|
||||
const PageTemplate = story<Args>((args) => ({
|
||||
component: CoreEmptyBoxPageComponent,
|
||||
props: {
|
||||
...args,
|
||||
content: marked(args.content),
|
||||
},
|
||||
}));
|
||||
|
||||
export const Primary = story<Args>(WrapperTemplate);
|
||||
|
||||
export const Example = story<Args>(PageTemplate, {
|
||||
icon: 'fas-magnifying-glass',
|
||||
content: '**No results for "Test Search"**\n\n<small>Check for typos or try using different keywords</small>',
|
||||
});
|
||||
|
||||
export const DimmedExample = story<Args>(PageTemplate, {
|
||||
icon: 'fas-magnifying-glass',
|
||||
content: 'What are you searching for?',
|
||||
dimmed: true,
|
||||
});
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { OnInit, Component } from '@angular/core';
|
||||
import { OnInit, Component, HostBinding } from '@angular/core';
|
||||
import { CoreBlockBaseComponent } from '../../classes/base-block-component';
|
||||
|
||||
/**
|
||||
|
@ -26,6 +26,8 @@ export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implem
|
|||
|
||||
courseId?: number;
|
||||
|
||||
@HostBinding('attr.id') id?: string;
|
||||
|
||||
constructor() {
|
||||
super('CoreBlockPreRenderedComponent');
|
||||
}
|
||||
|
@ -39,6 +41,8 @@ export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implem
|
|||
this.courseId = this.contextLevel == 'course' ? this.instanceId : undefined;
|
||||
|
||||
this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.';
|
||||
|
||||
this.id = `block-${this.block.instanceid}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ViewChildren, Input, OnInit, QueryList } from '@angular/core';
|
||||
import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core';
|
||||
import { ModalController } from '@singletons';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
|
||||
|
@ -22,6 +22,7 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreCoursesDashboard } from '@features/courses/services/dashboard';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreDom } from '@singletons/dom';
|
||||
|
||||
/**
|
||||
* Component that displays the list of side blocks.
|
||||
|
@ -35,6 +36,7 @@ export class CoreBlockSideBlocksComponent implements OnInit {
|
|||
|
||||
@Input() contextLevel!: string;
|
||||
@Input() instanceId!: number;
|
||||
@Input() initialBlockInstanceId?: number;
|
||||
@Input() myDashboardPage?: string;
|
||||
|
||||
@ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
|
||||
|
@ -42,12 +44,16 @@ export class CoreBlockSideBlocksComponent implements OnInit {
|
|||
loaded = false;
|
||||
blocks: CoreCourseBlock[] = [];
|
||||
|
||||
constructor(protected elementRef: ElementRef<HTMLElement>) {}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.loadContent().finally(() => {
|
||||
this.loaded = true;
|
||||
|
||||
this.focusInitialBlock();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -119,4 +125,20 @@ export class CoreBlockSideBlocksComponent implements OnInit {
|
|||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus the initial block, if any.
|
||||
*/
|
||||
private async focusInitialBlock(): Promise<void> {
|
||||
if (!this.initialBlockInstanceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = '#block-' + this.initialBlockInstanceId;
|
||||
|
||||
await CoreUtils.waitFor(() => !!this.elementRef.nativeElement.querySelector(selector));
|
||||
await CoreUtils.wait(200);
|
||||
|
||||
CoreDom.scrollToElement(this.elementRef.nativeElement, selector, { addYAxis: -10 });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ import { CoreDom } from '@singletons/dom';
|
|||
import { CoreUserTourDirectiveOptions } from '@directives/user-tour';
|
||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||
import { CorePlatform } from '@services/platform';
|
||||
import { CoreBlockSideBlocksComponent } from '@features/block/components/side-blocks/side-blocks';
|
||||
|
||||
/**
|
||||
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||
|
@ -76,6 +77,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
@Input() sections: CoreCourseSectionToDisplay[] = []; // List of course sections.
|
||||
@Input() initialSectionId?: number; // The section to load first (by ID).
|
||||
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
||||
@Input() initialBlockInstanceId?: number; // The instance to focus.
|
||||
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||
@Input() isGuest?: boolean; // If user is accessing using an ACCESS_GUEST enrolment method.
|
||||
|
||||
|
@ -298,16 +300,26 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
// Always load "All sections" to display the section title. If it isn't there just load the section.
|
||||
this.loaded = true;
|
||||
this.sectionChanged(sections[0]);
|
||||
} else if (this.initialSectionId || this.initialSectionNumber) {
|
||||
} else if (this.initialSectionId || this.initialSectionNumber !== undefined) {
|
||||
// We have an input indicating the section ID to load. Search the section.
|
||||
const section = sections.find((section) =>
|
||||
section.id == this.initialSectionId || (section.section && section.section == this.initialSectionNumber));
|
||||
section.id == this.initialSectionId ||
|
||||
(section.section !== undefined && section.section == this.initialSectionNumber));
|
||||
|
||||
// Don't load the section if it cannot be viewed by the user.
|
||||
if (section && this.canViewSection(section)) {
|
||||
this.loaded = true;
|
||||
this.sectionChanged(section);
|
||||
}
|
||||
} else if (this.initialBlockInstanceId && this.displayBlocks && this.hasBlocks) {
|
||||
CoreDomUtils.openSideModal({
|
||||
component: CoreBlockSideBlocksComponent,
|
||||
componentProps: {
|
||||
contextLevel: 'course',
|
||||
instanceId: this.course.id,
|
||||
initialBlockInstanceId: this.initialBlockInstanceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.loaded) {
|
||||
|
@ -666,8 +678,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
|||
CoreCourse.logView(this.course.id, sectionNumber),
|
||||
);
|
||||
|
||||
let extraParams = sectionNumber ? `§ion=${sectionNumber}` : '';
|
||||
if (firstLoad && sectionNumber) {
|
||||
let extraParams = sectionNumber !== undefined ? `§ion=${sectionNumber}` : '';
|
||||
if (firstLoad && sectionNumber !== undefined) {
|
||||
// If course is configured to show all sections in one page, don't include section in URL in first load.
|
||||
const courseDisplay = 'courseformatoptions' in this.course &&
|
||||
this.course.courseformatoptions?.find(option => option.name === 'coursedisplay');
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
<core-loading [hideUntil]="dataLoaded && !updatingData">
|
||||
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber"
|
||||
[moduleId]="moduleId" class="core-course-format-{{course.format}}" *ngIf="dataLoaded && sections" [isGuest]="isGuest">
|
||||
[initialBlockInstanceId]="blockInstanceId" [moduleId]="moduleId" class="core-course-format-{{course.format}}"
|
||||
*ngIf="dataLoaded && sections" [isGuest]="isGuest">
|
||||
</core-course-format>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
|
|
@ -58,6 +58,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
|
|||
sections?: CoreCourseSection[];
|
||||
sectionId?: number;
|
||||
sectionNumber?: number;
|
||||
blockInstanceId?: number;
|
||||
dataLoaded = false;
|
||||
updatingData = false;
|
||||
downloadCourseEnabled = false;
|
||||
|
@ -92,6 +93,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
|
|||
|
||||
this.sectionId = CoreNavigator.getRouteNumberParam('sectionId');
|
||||
this.sectionNumber = CoreNavigator.getRouteNumberParam('sectionNumber');
|
||||
this.blockInstanceId = CoreNavigator.getRouteNumberParam('blockInstanceId');
|
||||
this.moduleId = CoreNavigator.getRouteNumberParam('moduleId');
|
||||
this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest');
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
if (data.sectionId) {
|
||||
this.contentsTab.pageParams.sectionId = data.sectionId;
|
||||
}
|
||||
if (data.sectionNumber) {
|
||||
if (data.sectionNumber !== undefined) {
|
||||
this.contentsTab.pageParams.sectionNumber = data.sectionNumber;
|
||||
}
|
||||
|
||||
|
@ -162,12 +162,13 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
|||
course: this.course,
|
||||
sectionId: CoreNavigator.getRouteNumberParam('sectionId'),
|
||||
sectionNumber: CoreNavigator.getRouteNumberParam('sectionNumber'),
|
||||
blockInstanceId: CoreNavigator.getRouteNumberParam('blockInstanceId'),
|
||||
isGuest: this.isGuest,
|
||||
};
|
||||
|
||||
if (this.module) {
|
||||
this.contentsTab.pageParams.moduleId = this.module.id;
|
||||
if (!this.contentsTab.pageParams.sectionId && !this.contentsTab.pageParams.sectionNumber) {
|
||||
if (!this.contentsTab.pageParams.sectionId && this.contentsTab.pageParams.sectionNumber === undefined) {
|
||||
// No section specified, use module section.
|
||||
this.contentsTab.pageParams.sectionId = this.module.section;
|
||||
}
|
||||
|
|
|
@ -69,6 +69,12 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler
|
|||
|
||||
if (!isNaN(sectionNumber)) {
|
||||
pageParams.sectionNumber = sectionNumber;
|
||||
} else {
|
||||
const matches = url.match(/#inst(\d+)/);
|
||||
|
||||
if (matches && matches[1]) {
|
||||
pageParams.blockInstanceId = parseInt(matches[1], 10);
|
||||
}
|
||||
}
|
||||
|
||||
return [{
|
||||
|
@ -136,7 +142,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler
|
|||
// Direct access.
|
||||
const course = await CoreUtils.ignoreErrors(CoreCourses.getUserCourse(courseId), { id: courseId });
|
||||
|
||||
CoreCourseHelper.openCourse(course, pageParams);
|
||||
CoreCourseHelper.openCourse(course, { params: pageParams });
|
||||
} else {
|
||||
this.navigateCourseSummary(courseId, pageParams);
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu';
|
|||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreCustomURLSchemes } from '@services/urlschemes';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
|
||||
|
@ -161,13 +160,10 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
|
|||
CoreCustomURLSchemes.treatHandleCustomURLError(error);
|
||||
});
|
||||
} else if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { // Check if it's a URL.
|
||||
// Check if the app can handle the URL.
|
||||
const treated = await CoreContentLinksHelper.handleLink(text, undefined, true, true);
|
||||
|
||||
if (!treated) {
|
||||
// Can't handle it, open it in browser.
|
||||
CoreSites.getCurrentSite()?.openInBrowserWithAutoLogin(text);
|
||||
}
|
||||
await CoreSites.visitLink(text, {
|
||||
checkRoot: true,
|
||||
openBrowserRoot: true,
|
||||
});
|
||||
} else {
|
||||
// It's not a URL, open it in a modal so the user can see it and copy it.
|
||||
CoreTextUtils.viewText(Translate.instant('core.qrscanner'), text, {
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {
|
||||
CoreSearchGlobalSearchResult,
|
||||
CoreSearchGlobalSearch,
|
||||
CORE_SEARCH_GLOBAL_SEARCH_PAGE_LENGTH,
|
||||
CoreSearchGlobalSearchFilters,
|
||||
} from '@features/search/services/global-search';
|
||||
import { CorePaginatedItemsManagerSource } from '@classes/items-management/paginated-items-manager-source';
|
||||
|
||||
/**
|
||||
* Provides a collection of global search results.
|
||||
*/
|
||||
export class CoreSearchGlobalSearchResultsSource extends CorePaginatedItemsManagerSource<CoreSearchGlobalSearchResult> {
|
||||
|
||||
private query: string;
|
||||
private filters: CoreSearchGlobalSearchFilters;
|
||||
private pagesLoaded = 0;
|
||||
private topResultsIds?: number[];
|
||||
|
||||
constructor(query: string, filters: CoreSearchGlobalSearchFilters) {
|
||||
super();
|
||||
|
||||
this.query = query;
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the source has an empty query.
|
||||
*
|
||||
* @returns Whether the source has an empty query.
|
||||
*/
|
||||
hasEmptyQuery(): boolean {
|
||||
return !this.query || this.query.trim().length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search query.
|
||||
*
|
||||
* @returns Search query.
|
||||
*/
|
||||
getQuery(): string {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search filters.
|
||||
*
|
||||
* @returns Search filters.
|
||||
*/
|
||||
getFilters(): CoreSearchGlobalSearchFilters {
|
||||
return this.filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set search query.
|
||||
*
|
||||
* @param query Search query.
|
||||
*/
|
||||
setQuery(query: string): void {
|
||||
this.query = query;
|
||||
|
||||
this.setDirty(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set search filters.
|
||||
*
|
||||
* @param filters Search filters.
|
||||
*/
|
||||
setFilters(filters: CoreSearchGlobalSearchFilters): void {
|
||||
this.filters = filters;
|
||||
|
||||
this.setDirty(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getPagesLoaded(): number {
|
||||
return this.pagesLoaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async reload(): Promise<void> {
|
||||
this.pagesLoaded = 0;
|
||||
|
||||
await super.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset collection data.
|
||||
*/
|
||||
reset(): void {
|
||||
this.pagesLoaded = 0;
|
||||
delete this.topResultsIds;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async loadPageItems(page: number): Promise<{ items: CoreSearchGlobalSearchResult[]; hasMoreItems: boolean }> {
|
||||
this.pagesLoaded++;
|
||||
|
||||
const results: CoreSearchGlobalSearchResult[] = [];
|
||||
|
||||
if (page === 0) {
|
||||
const topResults = await CoreSearchGlobalSearch.getTopResults(this.query, this.filters);
|
||||
|
||||
results.push(...topResults);
|
||||
|
||||
this.topResultsIds = topResults.map(result => result.id);
|
||||
}
|
||||
|
||||
const pageResults = await CoreSearchGlobalSearch.getResults(this.query, this.filters, page);
|
||||
|
||||
results.push(...pageResults.results.filter(result => !this.topResultsIds?.includes(result.id)));
|
||||
|
||||
return { items: results, hasMoreItems: pageResults.canLoadMore };
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getPageLength(): number {
|
||||
return CORE_SEARCH_GLOBAL_SEARCH_PAGE_LENGTH;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { CoreEnrolledCourseData, CoreCourses } from '@features/courses/services/courses';
|
||||
import {
|
||||
CoreSearchGlobalSearchFilters,
|
||||
CoreSearchGlobalSearch,
|
||||
CoreSearchGlobalSearchSearchAreaCategory,
|
||||
CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED,
|
||||
} from '@features/search/services/global-search';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { ModalController } from '@singletons';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
type Filter<T=unknown> = T & { checked: boolean };
|
||||
|
||||
@Component({
|
||||
selector: 'core-search-global-search-filters',
|
||||
templateUrl: 'global-search-filters.html',
|
||||
styleUrls: ['./global-search-filters.scss'],
|
||||
})
|
||||
export class CoreSearchGlobalSearchFiltersComponent implements OnInit {
|
||||
|
||||
allSearchAreaCategories: boolean | null = true;
|
||||
searchAreaCategories: Filter<CoreSearchGlobalSearchSearchAreaCategory>[] = [];
|
||||
allCourses: boolean | null = true;
|
||||
courses: Filter<CoreEnrolledCourseData>[] = [];
|
||||
|
||||
@Input() filters?: CoreSearchGlobalSearchFilters;
|
||||
|
||||
private newFilters: CoreSearchGlobalSearchFilters = {};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.newFilters = this.filters ?? {};
|
||||
|
||||
await this.updateSearchAreaCategories();
|
||||
await this.updateCourses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close popover.
|
||||
*/
|
||||
close(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for all search area categories has been updated.
|
||||
*/
|
||||
allSearchAreaCategoriesUpdated(): void {
|
||||
if (this.allSearchAreaCategories === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checked = this.allSearchAreaCategories;
|
||||
|
||||
this.searchAreaCategories.forEach(searchAreaCategory => {
|
||||
if (searchAreaCategory.checked === checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchAreaCategory.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for one search area category has been updated.
|
||||
*
|
||||
* @param searchAreaCategory Filter status.
|
||||
*/
|
||||
onSearchAreaCategoryInputChanged(searchAreaCategory: Filter<CoreSearchGlobalSearchSearchAreaCategory>): void {
|
||||
if (
|
||||
!searchAreaCategory.checked &&
|
||||
this.newFilters.searchAreaCategoryIds &&
|
||||
!this.newFilters.searchAreaCategoryIds.includes(searchAreaCategory.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
searchAreaCategory.checked &&
|
||||
(!this.newFilters.searchAreaCategoryIds || this.newFilters.searchAreaCategoryIds.includes(searchAreaCategory.id))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.searchAreaCategoryUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for all courses has been updated.
|
||||
*/
|
||||
allCoursesUpdated(): void {
|
||||
if (this.allCourses === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checked = this.allCourses;
|
||||
|
||||
this.courses.forEach(course => {
|
||||
if (course.checked === checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
course.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for one course has been updated.
|
||||
*
|
||||
* @param course Filter status.
|
||||
*/
|
||||
onCourseInputChanged(course: Filter<CoreEnrolledCourseData>): void {
|
||||
if (!course.checked && this.newFilters.courseIds && !this.newFilters.courseIds.includes(course.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (course.checked && (!this.newFilters.courseIds || this.newFilters.courseIds.includes(course.id))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.courseUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh filters.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshFilters(refresher?: IonRefresher): Promise<void> {
|
||||
await CoreUtils.ignoreErrors(Promise.all([
|
||||
CoreSearchGlobalSearch.invalidateSearchAreas(),
|
||||
CoreCourses.invalidateUserCourses(),
|
||||
]));
|
||||
|
||||
await this.updateSearchAreaCategories();
|
||||
await this.updateCourses();
|
||||
|
||||
refresher?.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search area categories.
|
||||
*/
|
||||
private async updateSearchAreaCategories(): Promise<void> {
|
||||
const searchAreas = await CoreSearchGlobalSearch.getSearchAreas();
|
||||
const searchAreaCategoryIds = new Set();
|
||||
|
||||
this.searchAreaCategories = [];
|
||||
|
||||
for (const searchArea of searchAreas) {
|
||||
if (searchAreaCategoryIds.has(searchArea.category.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchAreaCategoryIds.add(searchArea.category.id);
|
||||
this.searchAreaCategories.push({
|
||||
...searchArea.category,
|
||||
checked: this.filters?.searchAreaCategoryIds?.includes(searchArea.category.id) ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
this.allSearchAreaCategories = this.getGroupFilterStatus(this.searchAreaCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update courses.
|
||||
*/
|
||||
private async updateCourses(): Promise<void> {
|
||||
const courses = await CoreCourses.getUserCourses();
|
||||
|
||||
this.courses = courses
|
||||
.sort((a, b) => (a.shortname?.toLowerCase() ?? '').localeCompare(b.shortname?.toLowerCase() ?? ''))
|
||||
.map(course => ({
|
||||
...course,
|
||||
checked: this.filters?.courseIds?.includes(course.id) ?? true,
|
||||
}));
|
||||
|
||||
this.allCourses = this.getGroupFilterStatus(this.courses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for one search area category has been updated.
|
||||
*/
|
||||
private searchAreaCategoryUpdated(): void {
|
||||
const filterStatus = this.getGroupFilterStatus(this.searchAreaCategories);
|
||||
|
||||
if (filterStatus !== this.allSearchAreaCategories) {
|
||||
this.allSearchAreaCategories = filterStatus;
|
||||
}
|
||||
|
||||
this.emitFiltersUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Course filter status has been updated.
|
||||
*/
|
||||
private courseUpdated(): void {
|
||||
const filterStatus = this.getGroupFilterStatus(this.courses);
|
||||
|
||||
if (filterStatus !== this.allCourses) {
|
||||
this.allCourses = filterStatus;
|
||||
}
|
||||
|
||||
this.emitFiltersUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status for a filter representing a group of filters.
|
||||
*
|
||||
* @param filters Filters in the group.
|
||||
* @returns Group filter status. This will be true if all filters are checked, false if all filters are unchecked,
|
||||
* or null if filters have mixed states.
|
||||
*/
|
||||
private getGroupFilterStatus(filters: Filter[]): boolean | null {
|
||||
if (filters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstChecked = filters[0].checked;
|
||||
|
||||
for (const filter of filters) {
|
||||
if (filter.checked === firstChecked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return firstChecked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit filters updated event.
|
||||
*/
|
||||
private emitFiltersUpdated(): void {
|
||||
this.newFilters = {};
|
||||
|
||||
if (!this.allSearchAreaCategories) {
|
||||
this.newFilters.searchAreaCategoryIds = this.searchAreaCategories.filter(({ checked }) => checked).map(({ id }) => id);
|
||||
}
|
||||
|
||||
if (!this.allCourses) {
|
||||
this.newFilters.courseIds = this.courses.filter(({ checked }) => checked).map(({ id }) => id);
|
||||
}
|
||||
|
||||
CoreEvents.trigger(CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED, this.newFilters);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h1>{{ 'core.search.filterheader' | translate }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="close()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="fas-xmark" slot="icon-only" aria-hidden=true></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content [fullscreen]="true">
|
||||
<ion-refresher slot="fixed" (ionRefresh)="refreshFilters($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<ion-list>
|
||||
<ng-container *ngIf="searchAreaCategories.length > 0">
|
||||
<core-spacer></core-spacer>
|
||||
<ion-item class="ion-text-wrap help">
|
||||
<ion-label>
|
||||
{{ 'core.search.filtercategories' | translate }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'core.search.allcategories' | translate }}</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="allSearchAreaCategories" [indeterminate]="allSearchAreaCategories === null"
|
||||
(ionChange)="allSearchAreaCategoriesUpdated()"></ion-checkbox>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let searchAreaCategory of searchAreaCategories">
|
||||
<ion-label>
|
||||
<core-format-text [text]="searchAreaCategory.name"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="searchAreaCategory.checked"
|
||||
(ionChange)="onSearchAreaCategoryInputChanged(searchAreaCategory)"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="courses.length > 0">
|
||||
<core-spacer></core-spacer>
|
||||
<ion-item class="ion-text-wrap help">
|
||||
<ion-label>
|
||||
{{ 'core.search.filtercourses' | translate }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'core.search.allcourses' | translate }}</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="allCourses" [indeterminate]="allCourses === null" (ionChange)="allCoursesUpdated()">
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let course of courses">
|
||||
<ion-label>
|
||||
<core-format-text [text]="course.shortname"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="course.checked" (ionChange)="onCourseInputChanged(course)"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</ion-content>
|
|
@ -0,0 +1,30 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { CoreSearchGlobalSearchFiltersComponent } from './global-search-filters.component';
|
||||
|
||||
export { CoreSearchGlobalSearchFiltersComponent };
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreSearchGlobalSearchFiltersComponent,
|
||||
],
|
||||
})
|
||||
export class CoreSearchGlobalSearchFiltersComponentModule {}
|
|
@ -0,0 +1,21 @@
|
|||
:host {
|
||||
--help-text-color: var(--gray-700);
|
||||
|
||||
ion-item.help {
|
||||
color: var(--help-text-color);
|
||||
|
||||
ion-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ion-item:not(.help) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
:host-context(html.dark) {
|
||||
--help-text-color: var(--gray-400);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<ion-item lines="inset" button (click)="onClick.emit($event)">
|
||||
<core-course-image *ngIf="result.course" [course]="result.course"></core-course-image>
|
||||
<core-user-avatar *ngIf="result.user" [user]="result.user" [linkProfile]="false"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h3 *ngIf="result.title">
|
||||
<ion-icon *ngIf="renderedIcon" [name]="renderedIcon" aria-hidden="true"></ion-icon>
|
||||
<core-mod-icon *ngIf="!renderedIcon && result.module" [modicon]="result.module.iconurl"
|
||||
[modname]="result.module.name"></core-mod-icon>
|
||||
<img *ngIf="!renderedIcon && !result.module && result.component" [src]="result.component.iconurl" alt="" class="result-icon"
|
||||
core-external-content [component]="result.component.name">
|
||||
<core-format-text [text]="result.title"></core-format-text>
|
||||
</h3>
|
||||
<core-format-text *ngIf="result.content && !result.course && !result.user" [text]="result.content"></core-format-text>
|
||||
<div *ngIf="result.context" class="flex-row">
|
||||
<div *ngIf="result.context.courseName" class="result-context">
|
||||
<ion-icon name="fas-graduation-cap" aria-hidden="true"></ion-icon>
|
||||
<core-format-text [text]="result.context.courseName"></core-format-text>
|
||||
</div>
|
||||
<div *ngIf="result.context.userName" class="result-context">
|
||||
<ion-icon name="fas-user" aria-hidden="true"></ion-icon>
|
||||
<span>{{ 'core.search.resultby' | translate: { $a: result.context.userName } }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
|
@ -0,0 +1,90 @@
|
|||
:host ion-item {
|
||||
--core-global-search-result-image-size: 40px;
|
||||
--core-global-search-result-title-color: var(--text);
|
||||
--core-global-search-result-content-color: var(--gray-700);
|
||||
--core-global-search-result-context-color: var(--gray-600);
|
||||
--core-global-search-result-icon-size: 16px;
|
||||
--mod-icon-filter: brightness(0);
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--core-global-search-result-title-color);
|
||||
|
||||
core-mod-icon {
|
||||
--size: var(--core-global-search-result-icon-size);
|
||||
--filter: var(--mod-icon-filter);
|
||||
|
||||
margin-inline-end: var(--spacing-2);
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
padding: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
ion-icon, .result-icon {
|
||||
width: var(--core-global-search-result-icon-size);
|
||||
height: var(--core-global-search-result-icon-size);
|
||||
|
||||
margin-inline-end: var(--spacing-2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
core-user-avatar {
|
||||
--core-avatar-size: var(--core-global-search-result-image-size);
|
||||
|
||||
margin-top: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
core-course-image {
|
||||
--core-image-size: var(--core-global-search-result-image-size);
|
||||
|
||||
margin-top: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
ion-label {
|
||||
|
||||
core-format-text {
|
||||
color: var(--core-global-search-result-content-color);
|
||||
|
||||
@supports (-webkit-line-clamp: 2) {
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.result-context {
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
color: var(--core-global-search-result-context-color);
|
||||
margin-top: var(--spacing-2);
|
||||
font-size: 12px;
|
||||
|
||||
ion-icon {
|
||||
margin-inline-end: var(--spacing-1);
|
||||
}
|
||||
|
||||
+ .result-context {
|
||||
margin-inline-start: var(--spacing-4);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
:host-context(html.dark) ion-item {
|
||||
--core-global-search-result-content-color: var(--gray-400);
|
||||
--core-global-search-result-context-color: var(--gray-500);
|
||||
--mod-icon-filter: brightness(0) invert(1);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, Output, EventEmitter, OnChanges } from '@angular/core';
|
||||
import { CoreSearchGlobalSearchResult } from '@features/search/services/global-search';
|
||||
|
||||
@Component({
|
||||
selector: 'core-search-global-search-result',
|
||||
templateUrl: 'global-search-result.html',
|
||||
styleUrls: ['./global-search-result.scss'],
|
||||
})
|
||||
export class CoreSearchGlobalSearchResultComponent implements OnChanges {
|
||||
|
||||
@Input() result!: CoreSearchGlobalSearchResult;
|
||||
renderedIcon: string | null = null;
|
||||
|
||||
@Output() onClick = new EventEmitter();
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(): void {
|
||||
this.renderedIcon = this.computeRenderedIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the value of the icon to render.
|
||||
*
|
||||
* @returns Rendered icon.
|
||||
*/
|
||||
private computeRenderedIcon(): string | null {
|
||||
return this.result.module?.name === 'forum' && this.result.module.area === 'post'
|
||||
? 'fa-message'
|
||||
: null;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"allcourses": "All courses",
|
||||
"allcategories": "All categories",
|
||||
"empty": "What are you searching for?",
|
||||
"filtercategories": "Filter results by",
|
||||
"filtercourses": "Search in",
|
||||
"filterheader": "Filter",
|
||||
"globalsearch": "Global search",
|
||||
"noresults": "No results for \"{{$a}}\"",
|
||||
"noresultshelp": "Check for typos or try using different keywords",
|
||||
"resultby": "By {{$a}}"
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<h1>{{ 'core.search.globalsearch' | translate }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="limited-width">
|
||||
<div>
|
||||
<ion-card class="core-danger-card" *ngIf="searchBanner">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<core-format-text [text]="searchBanner"></core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch()" [placeholder]="'core.search' | translate"
|
||||
[searchLabel]="'core.search' | translate" [autoFocus]="true" searchArea="CoreSearchGlobalSearch"></core-search-box>
|
||||
|
||||
<ion-list *ngIf="resultsSource.isLoaded()">
|
||||
<core-search-global-search-result *ngFor="let result of resultsSource.getItems()" [result]="result"
|
||||
(onClick)="visitResult(result)">
|
||||
</core-search-global-search-result>
|
||||
</ion-list>
|
||||
|
||||
<core-infinite-loading [enabled]="resultsSource.isLoaded() && !resultsSource.isCompleted()" (action)="loadMoreResults($event)"
|
||||
[error]="loadMoreError">
|
||||
</core-infinite-loading>
|
||||
|
||||
<core-empty-box *ngIf="resultsSource.isEmpty()" icon="fas-magnifying-glass" [dimmed]="!resultsSource.isLoaded()">
|
||||
<p *ngIf="!resultsSource.isLoaded()">{{ 'core.search.empty' | translate }}</p>
|
||||
<ng-container *ngIf="resultsSource.isLoaded()">
|
||||
<p><strong>{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}</strong></p>
|
||||
<p><small>{{ 'core.search.noresultshelp' | translate }}</small></p>
|
||||
</ng-container>
|
||||
</core-empty-box>
|
||||
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<ion-fab-button (click)="openFilters()" [attr.aria-label]="'core.filter' | translate">
|
||||
<ion-icon name="fas-filter" aria-hidden="true"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</div>
|
||||
</ion-content>
|
|
@ -0,0 +1,174 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreSearchGlobalSearchResultsSource } from '@features/search/classes/global-search-results-source';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreEvents, CoreEventObserver } from '@singletons/events';
|
||||
import {
|
||||
CoreSearchGlobalSearchResult,
|
||||
CoreSearchGlobalSearchFilters,
|
||||
CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED,
|
||||
CoreSearchGlobalSearch,
|
||||
} from '@features/search/services/global-search';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSearchBoxComponent } from '@features/search/components/search-box/search-box';
|
||||
|
||||
@Component({
|
||||
selector: 'page-core-search-global-search',
|
||||
templateUrl: 'global-search.html',
|
||||
})
|
||||
export class CoreSearchGlobalSearchPage implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
loadMoreError: string | null = null;
|
||||
searchBanner: string | null = null;
|
||||
resultsSource = new CoreSearchGlobalSearchResultsSource('', {});
|
||||
private filtersObserver?: CoreEventObserver;
|
||||
|
||||
@ViewChild(CoreSearchBoxComponent) searchBox?: CoreSearchBoxComponent;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const site = CoreSites.getRequiredCurrentSite();
|
||||
const searchBanner = site.config?.searchbanner?.trim() ?? '';
|
||||
const courseId = CoreNavigator.getRouteNumberParam('courseId');
|
||||
|
||||
if (CoreUtils.isTrueOrOne(site.config?.searchbannerenable) && searchBanner.length > 0) {
|
||||
this.searchBanner = searchBanner;
|
||||
}
|
||||
|
||||
if (courseId) {
|
||||
this.resultsSource.setFilters({ courseIds: [courseId] });
|
||||
}
|
||||
|
||||
this.filtersObserver = CoreEvents.on(
|
||||
CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED,
|
||||
filters => this.resultsSource.setFilters(filters),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
const query = CoreNavigator.getRouteParam('query');
|
||||
|
||||
if (query) {
|
||||
if (this.searchBox) {
|
||||
this.searchBox.searchText = query;
|
||||
}
|
||||
|
||||
this.search(query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.filtersObserver?.off();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a new search.
|
||||
*
|
||||
* @param query Search query.
|
||||
*/
|
||||
async search(query: string): Promise<void> {
|
||||
this.resultsSource.setQuery(query);
|
||||
|
||||
if (this.resultsSource.hasEmptyQuery()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await CoreDomUtils.showOperationModals('core.searching', true, async () => {
|
||||
await this.resultsSource.reload();
|
||||
await CoreUtils.ignoreErrors(
|
||||
CoreSearchGlobalSearch.logViewResults(this.resultsSource.getQuery(), this.resultsSource.getFilters()),
|
||||
);
|
||||
|
||||
CoreAnalytics.logEvent({
|
||||
type: CoreAnalyticsEventType.VIEW_ITEM_LIST,
|
||||
ws: 'core_search_view_results',
|
||||
name: Translate.instant('core.search.globalsearch'),
|
||||
data: {
|
||||
query,
|
||||
filters: JSON.stringify(this.resultsSource.getFilters()),
|
||||
},
|
||||
url: CoreUrlUtils.addParamsToUrl('/search/index.php', {
|
||||
q: query,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear search results.
|
||||
*/
|
||||
clearSearch(): void {
|
||||
this.loadMoreError = null;
|
||||
|
||||
this.resultsSource.setQuery('');
|
||||
this.resultsSource.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open filters.
|
||||
*/
|
||||
async openFilters(): Promise<void> {
|
||||
const { CoreSearchGlobalSearchFiltersComponent } =
|
||||
await import('@features/search/components/global-search-filters/global-search-filters.module');
|
||||
|
||||
await CoreDomUtils.openSideModal<CoreSearchGlobalSearchFilters>({
|
||||
component: CoreSearchGlobalSearchFiltersComponent,
|
||||
componentProps: { filters: this.resultsSource.getFilters() },
|
||||
});
|
||||
|
||||
if (!this.resultsSource.hasEmptyQuery() && this.resultsSource.isDirty()) {
|
||||
await CoreDomUtils.showOperationModals('core.searching', true, () => this.resultsSource.reload());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit a result's origin.
|
||||
*
|
||||
* @param result Result to visit.
|
||||
*/
|
||||
async visitResult(result: CoreSearchGlobalSearchResult): Promise<void> {
|
||||
await CoreSites.visitLink(result.url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more results.
|
||||
*
|
||||
* @param complete Notify completion.
|
||||
*/
|
||||
async loadMoreResults(complete: () => void ): Promise<void> {
|
||||
try {
|
||||
await this.resultsSource?.load();
|
||||
} catch (error) {
|
||||
this.loadMoreError = CoreDomUtils.getErrorMessage(error);
|
||||
} finally {
|
||||
complete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { NgModule, Injector } from '@angular/core';
|
||||
import { RouterModule, Routes, ROUTES } from '@angular/router';
|
||||
import { CoreSearchGlobalSearchPage } from './pages/global-search/global-search';
|
||||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreSearchGlobalSearchResultComponent } from '@features/search/components/global-search-result/global-search-result';
|
||||
|
||||
/**
|
||||
* Build module routes.
|
||||
*
|
||||
* @param injector Injector.
|
||||
* @returns Routes.
|
||||
*/
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
return buildTabMainRoutes(injector, {
|
||||
component: CoreSearchGlobalSearchPage,
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreSearchComponentsModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
declarations: [
|
||||
CoreSearchGlobalSearchPage,
|
||||
CoreSearchGlobalSearchResultComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: ROUTES,
|
||||
multi: true,
|
||||
deps: [Injector],
|
||||
useFactory: buildRoutes,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CoreSearchLazyModule {}
|
|
@ -12,24 +12,50 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule, Type } from '@angular/core';
|
||||
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
|
||||
import { CoreSearchGlobalSearchService } from '@features/search/services/global-search';
|
||||
import { CoreSearchMainMenuHandler, CORE_SEARCH_PAGE_NAME } from '@features/search/services/handlers/mainmenu';
|
||||
|
||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||
|
||||
import { CoreSearchComponentsModule } from './components/components.module';
|
||||
import { SITE_SCHEMA } from './services/search-history-db';
|
||||
import { CoreSearchHistoryProvider } from './services/search-history.service';
|
||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreSearchGlobalSearchLinkHandler } from '@features/search/services/handlers/global-search-link';
|
||||
|
||||
export const CORE_SEARCH_SERVICES: Type<unknown>[] = [
|
||||
CoreSearchHistoryProvider,
|
||||
CoreSearchGlobalSearchService,
|
||||
];
|
||||
|
||||
const mainMenuChildrenRoutes: Routes = [
|
||||
{
|
||||
path: CORE_SEARCH_PAGE_NAME,
|
||||
loadChildren: () => import('./search-lazy.module').then(m => m.CoreSearchLazyModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreSearchComponentsModule,
|
||||
CoreMainMenuTabRoutingModule.forChild(mainMenuChildrenRoutes),
|
||||
CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }),
|
||||
],
|
||||
providers: [
|
||||
{ provide: CORE_SITE_SCHEMAS, useValue: [SITE_SCHEMA], multi: true },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue() {
|
||||
CoreMainMenuDelegate.registerHandler(CoreSearchMainMenuHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(CoreSearchGlobalSearchLinkHandler.instance);
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CoreSearchModule {}
|
||||
|
|
|
@ -0,0 +1,420 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreWSExternalWarning } from '@services/ws';
|
||||
import { CoreCourseListItem, CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreSite } from '@classes/site';
|
||||
|
||||
declare module '@singletons/events' {
|
||||
|
||||
/**
|
||||
* Augment CoreEventsData interface with events specific to this service.
|
||||
*
|
||||
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||
*/
|
||||
export interface CoreEventsData {
|
||||
[CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED]: CoreSearchGlobalSearchFilters;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CORE_SEARCH_GLOBAL_SEARCH_PAGE_LENGTH = 10;
|
||||
export const CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED = 'core-search-global-search-filters-updated';
|
||||
|
||||
export type CoreSearchGlobalSearchResult = {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
content?: string;
|
||||
context?: CoreSearchGlobalSearchResultContext;
|
||||
module?: CoreSearchGlobalSearchResultModule;
|
||||
component?: CoreSearchGlobalSearchResultComponent;
|
||||
course?: CoreCourseListItem;
|
||||
user?: CoreUserWithAvatar;
|
||||
};
|
||||
|
||||
export type CoreSearchGlobalSearchResultContext = {
|
||||
userName?: string;
|
||||
courseName?: string;
|
||||
};
|
||||
|
||||
export type CoreSearchGlobalSearchResultModule = {
|
||||
name: string;
|
||||
iconurl: string;
|
||||
area: string;
|
||||
};
|
||||
|
||||
export type CoreSearchGlobalSearchResultComponent = {
|
||||
name: string;
|
||||
iconurl: string;
|
||||
};
|
||||
|
||||
export type CoreSearchGlobalSearchSearchAreaCategory = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CoreSearchGlobalSearchSearchArea = {
|
||||
id: string;
|
||||
name: string;
|
||||
category: CoreSearchGlobalSearchSearchAreaCategory;
|
||||
};
|
||||
|
||||
export interface CoreSearchGlobalSearchFilters {
|
||||
searchAreaCategoryIds?: string[];
|
||||
courseIds?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to perform global searches.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreSearchGlobalSearchService {
|
||||
|
||||
private static readonly SEARCH_AREAS_CACHE_KEY = 'CoreSearchGlobalSearch:SearchAreas';
|
||||
|
||||
/**
|
||||
* Check whether global search is enabled or not.
|
||||
*
|
||||
* @returns Whether global search is enabled or not.
|
||||
*/
|
||||
async isEnabled(siteId?: string): Promise<boolean> {
|
||||
const site = siteId
|
||||
? await CoreSites.getSite(siteId)
|
||||
: CoreSites.getRequiredCurrentSite();
|
||||
|
||||
return !site?.isFeatureDisabled('CoreNoDelegate_GlobalSearch')
|
||||
&& site?.wsAvailable('core_search_get_results') // @since 4.3
|
||||
&& site?.canUseAdvancedFeature('enableglobalsearch');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get results.
|
||||
*
|
||||
* @param query Search query.
|
||||
* @param filters Search filters.
|
||||
* @param page Page.
|
||||
* @returns Search results.
|
||||
*/
|
||||
async getResults(
|
||||
query: string,
|
||||
filters: CoreSearchGlobalSearchFilters,
|
||||
page: number,
|
||||
): Promise<{ results: CoreSearchGlobalSearchResult[]; canLoadMore: boolean }> {
|
||||
if (this.filtersYieldEmptyResults(filters)) {
|
||||
return {
|
||||
results: [],
|
||||
canLoadMore: false,
|
||||
};
|
||||
}
|
||||
|
||||
const site = CoreSites.getRequiredCurrentSite();
|
||||
const params: CoreSearchGetResultsWSParams = {
|
||||
query,
|
||||
page,
|
||||
filters: await this.prepareWSFilters(filters),
|
||||
};
|
||||
const preSets = CoreSites.getReadingStrategyPreSets(CoreSitesReadingStrategy.PREFER_NETWORK);
|
||||
|
||||
const { totalcount, results } = await site.read<CoreSearchGetResultsWSResponse>('core_search_get_results', params, preSets);
|
||||
|
||||
return {
|
||||
results: await Promise.all((results ?? []).map(result => this.formatWSResult(result))),
|
||||
canLoadMore: totalcount > (page + 1) * CORE_SEARCH_GLOBAL_SEARCH_PAGE_LENGTH,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top results.
|
||||
*
|
||||
* @param query Search query.
|
||||
* @param filters Search filters.
|
||||
* @returns Top search results.
|
||||
*/
|
||||
async getTopResults(query: string, filters: CoreSearchGlobalSearchFilters): Promise<CoreSearchGlobalSearchResult[]> {
|
||||
if (this.filtersYieldEmptyResults(filters)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const site = CoreSites.getRequiredCurrentSite();
|
||||
const params: CoreSearchGetTopResultsWSParams = {
|
||||
query,
|
||||
filters: await this.prepareWSFilters(filters),
|
||||
};
|
||||
const preSets = CoreSites.getReadingStrategyPreSets(CoreSitesReadingStrategy.PREFER_NETWORK);
|
||||
|
||||
const { results } = await site.read<CoreSearchGetTopResultsWSResponse>('core_search_get_top_results', params, preSets);
|
||||
|
||||
return await Promise.all((results ?? []).map(result => this.formatWSResult(result)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available search areas.
|
||||
*
|
||||
* @returns Search areas.
|
||||
*/
|
||||
async getSearchAreas(): Promise<CoreSearchGlobalSearchSearchArea[]> {
|
||||
const site = CoreSites.getRequiredCurrentSite();
|
||||
const params: CoreSearchGetSearchAreasListWSParams = {};
|
||||
|
||||
const { areas } = await site.read<CoreSearchGetSearchAreasListWSResponse>('core_search_get_search_areas_list', params, {
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
cacheKey: CoreSearchGlobalSearchService.SEARCH_AREAS_CACHE_KEY,
|
||||
});
|
||||
|
||||
return areas.map(area => ({
|
||||
id: area.id,
|
||||
name: area.name,
|
||||
category: {
|
||||
id: area.categoryid,
|
||||
name: area.categoryname,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate search areas cache.
|
||||
*/
|
||||
async invalidateSearchAreas(): Promise<void> {
|
||||
const site = CoreSites.getRequiredCurrentSite();
|
||||
|
||||
await site.invalidateWsCacheForKey(CoreSearchGlobalSearchService.SEARCH_AREAS_CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log event for viewing results.
|
||||
*
|
||||
* @param query Search query.
|
||||
* @param filters Search filters.
|
||||
*/
|
||||
async logViewResults(query: string, filters: CoreSearchGlobalSearchFilters): Promise<void> {
|
||||
const site = CoreSites.getRequiredCurrentSite();
|
||||
const params: CoreSearchViewResultsWSParams = {
|
||||
query,
|
||||
filters: await this.prepareWSFilters(filters),
|
||||
};
|
||||
|
||||
await site.write<CoreSearchViewResultsWSResponse>('core_search_view_results', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a WS result to be used in the app.
|
||||
*
|
||||
* @param wsResult WS result.
|
||||
* @returns App result.
|
||||
*/
|
||||
protected async formatWSResult(wsResult: CoreSearchWSResult): Promise<CoreSearchGlobalSearchResult> {
|
||||
const result: CoreSearchGlobalSearchResult = {
|
||||
id: wsResult.itemid,
|
||||
title: wsResult.title,
|
||||
url: wsResult.docurl,
|
||||
content: wsResult.content,
|
||||
};
|
||||
|
||||
if (wsResult.componentname === 'core_user') {
|
||||
const user = await CoreUser.getProfile(wsResult.itemid);
|
||||
|
||||
result.user = user;
|
||||
} else if (wsResult.componentname === 'core_course' && wsResult.areaname === 'course') {
|
||||
const course = await CoreCourses.getCourseByField('id', wsResult.itemid);
|
||||
|
||||
result.course = course;
|
||||
} else {
|
||||
if (wsResult.userfullname || wsResult.coursefullname) {
|
||||
result.context = {
|
||||
userName: wsResult.userfullname,
|
||||
courseName: wsResult.coursefullname,
|
||||
};
|
||||
}
|
||||
|
||||
if (wsResult.iconurl) {
|
||||
if (wsResult.componentname.startsWith('mod_')) {
|
||||
result.module = {
|
||||
name: wsResult.componentname.substring(4),
|
||||
iconurl: wsResult.iconurl,
|
||||
area: wsResult.areaname,
|
||||
};
|
||||
} else {
|
||||
result.component = {
|
||||
name: wsResult.componentname,
|
||||
iconurl: wsResult.iconurl,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given filter will necessarily yield an empty list of results.
|
||||
*
|
||||
* @param filters Filters.
|
||||
* @returns Whether the given filters will return 0 results.
|
||||
*/
|
||||
protected filtersYieldEmptyResults(filters: CoreSearchGlobalSearchFilters): boolean {
|
||||
return filters.courseIds?.length === 0 || filters.searchAreaCategoryIds?.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare search filters before sending to WS.
|
||||
*
|
||||
* @param filters App filters.
|
||||
* @returns WS filters.
|
||||
*/
|
||||
protected async prepareWSFilters(filters: CoreSearchGlobalSearchFilters): Promise<CoreSearchBasicWSFilters> {
|
||||
const wsFilters: CoreSearchBasicWSFilters = {};
|
||||
|
||||
if (filters.courseIds) {
|
||||
wsFilters.courseids = filters.courseIds;
|
||||
}
|
||||
|
||||
if (filters.searchAreaCategoryIds) {
|
||||
const searchAreas = await this.getSearchAreas();
|
||||
|
||||
wsFilters.areaids = searchAreas
|
||||
.filter(({ category }) => filters.searchAreaCategoryIds?.includes(category.id))
|
||||
.map(({ id }) => id);
|
||||
}
|
||||
|
||||
return wsFilters;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreSearchGlobalSearch = makeSingleton(CoreSearchGlobalSearchService);
|
||||
|
||||
/**
|
||||
* Params of core_search_get_results WS.
|
||||
*/
|
||||
type CoreSearchGetResultsWSParams = {
|
||||
query: string; // The search query.
|
||||
filters?: CoreSearchAdvancedWSFilters; // Filters to apply.
|
||||
page?: number; // Results page number starting from 0, defaults to the first page.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_search_get_search_areas_list WS.
|
||||
*/
|
||||
type CoreSearchGetSearchAreasListWSParams = {
|
||||
cat?: string; // Category to filter areas.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_search_view_results WS.
|
||||
*/
|
||||
type CoreSearchViewResultsWSParams = {
|
||||
query: string; // The search query.
|
||||
filters?: CoreSearchBasicWSFilters; // Filters to apply.
|
||||
page?: number; // Results page number starting from 0, defaults to the first page.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_search_get_top_results WS.
|
||||
*/
|
||||
type CoreSearchGetTopResultsWSParams = {
|
||||
query: string; // The search query.
|
||||
filters?: CoreSearchAdvancedWSFilters; // Filters to apply.
|
||||
};
|
||||
|
||||
/**
|
||||
* Search result returned in WS.
|
||||
*/
|
||||
type CoreSearchWSResult = { // Search results.
|
||||
itemid: number; // Unique id in the search area scope.
|
||||
componentname: string; // Component name.
|
||||
areaname: string; // Search area name.
|
||||
courseurl: string; // Result course url.
|
||||
coursefullname: string; // Result course fullname.
|
||||
timemodified: number; // Result modified time.
|
||||
title: string; // Result title.
|
||||
docurl: string; // Result url.
|
||||
iconurl?: string; // Icon url.
|
||||
content?: string; // Result contents.
|
||||
contextid: number; // Result context id.
|
||||
contexturl: string; // Result context url.
|
||||
description1?: string; // Extra result contents, depends on the search area.
|
||||
description2?: string; // Extra result contents, depends on the search area.
|
||||
multiplefiles?: number; // Whether multiple files are returned or not.
|
||||
filenames?: string[]; // Result file names if present.
|
||||
filename?: string; // Result file name if present.
|
||||
userid?: number; // User id.
|
||||
userurl?: string; // User url.
|
||||
userfullname?: string; // User fullname.
|
||||
textformat: number; // Text fields format, it is the same for all of them.
|
||||
};
|
||||
|
||||
/**
|
||||
* Basic search filters used in WS.
|
||||
*/
|
||||
type CoreSearchBasicWSFilters = {
|
||||
title?: string; // Result title.
|
||||
areaids?: string[]; // Restrict results to these areas.
|
||||
courseids?: number[]; // Restrict results to these courses.
|
||||
timestart?: number; // Docs modified after this date.
|
||||
timeend?: number; // Docs modified before this date.
|
||||
};
|
||||
|
||||
/**
|
||||
* Advanced search filters used in WS.
|
||||
*/
|
||||
type CoreSearchAdvancedWSFilters = CoreSearchBasicWSFilters & {
|
||||
contextids?: number[]; // Restrict results to these contexts.
|
||||
cat?: string; // Category to filter areas.
|
||||
userids?: number[]; // Restrict results to these users.
|
||||
groupids?: number[]; // Restrict results to these groups.
|
||||
mycoursesonly?: boolean; // Only results from my courses.
|
||||
order?: string; // How to order.
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by core_search_get_results WS.
|
||||
*/
|
||||
type CoreSearchGetResultsWSResponse = {
|
||||
totalcount: number; // Total number of results.
|
||||
results?: CoreSearchWSResult[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by core_search_get_search_areas_list WS.
|
||||
*/
|
||||
type CoreSearchGetSearchAreasListWSResponse = {
|
||||
areas: { // Search areas.
|
||||
id: string; // Search area id.
|
||||
categoryid: string; // Category id.
|
||||
categoryname: string; // Category name.
|
||||
name: string; // Search area name.
|
||||
}[];
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by core_search_view_results WS.
|
||||
*/
|
||||
type CoreSearchViewResultsWSResponse = {
|
||||
status: boolean; // Status: true if success.
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by core_search_get_top_results WS.
|
||||
*/
|
||||
type CoreSearchGetTopResultsWSResponse = {
|
||||
results?: CoreSearchWSResult[];
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreSearchGlobalSearch } from '@features/search/services/global-search';
|
||||
import { CORE_SEARCH_PAGE_NAME } from '@features/search/services/handlers/mainmenu';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
* Handler to treat links to search page.
|
||||
*/
|
||||
@Injectable( { providedIn: 'root' })
|
||||
export class CoreSearchGlobalSearchLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||
|
||||
name = 'CoreSearchSearchLinkHandler';
|
||||
pattern = /\/search\/index\.php.*/;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(siteId: string): Promise<boolean> {
|
||||
return CoreSearchGlobalSearch.isEnabled(siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getActions(siteIds: string[], url: string, params: Record<string, string>): CoreContentLinksAction[] {
|
||||
return [{
|
||||
action: (siteId: string): void => {
|
||||
CoreNavigator.navigateToSitePath(CORE_SEARCH_PAGE_NAME, {
|
||||
siteId,
|
||||
params: {
|
||||
query: params.q,
|
||||
},
|
||||
});
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
}
|
||||
export const CoreSearchGlobalSearchLinkHandler = makeSingleton(CoreSearchGlobalSearchLinkHandlerService);
|
|
@ -0,0 +1,52 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate';
|
||||
import { CoreSearchGlobalSearch } from '@features/search/services/global-search';
|
||||
|
||||
export const CORE_SEARCH_PAGE_NAME = 'search';
|
||||
|
||||
/**
|
||||
* Handler to inject an option into main menu.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreSearchMainMenuHandlerService implements CoreMainMenuHandler {
|
||||
|
||||
name = 'CoreSearch';
|
||||
priority = 575;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
return CoreSearchGlobalSearch.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDisplayData(): CoreMainMenuHandlerData {
|
||||
return {
|
||||
icon: 'fas-magnifying-glass',
|
||||
title: 'core.search.globalsearch',
|
||||
page: CORE_SEARCH_PAGE_NAME,
|
||||
class: 'core-search-handler',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreSearchMainMenuHandler = makeSingleton(CoreSearchMainMenuHandlerService);
|
|
@ -0,0 +1,39 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { StorybookModule } from '@/storybook/storybook.module';
|
||||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
CoreSearchGlobalSearchResultsPageComponent,
|
||||
} from '@features/search/stories/components/global-search-results-page/global-search-results-page';
|
||||
import { CoreSearchGlobalSearchResultComponent } from '@features/search/components/global-search-result/global-search-result';
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreSearchGlobalSearchResultsPageComponent,
|
||||
CoreSearchGlobalSearchResultComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CommonModule,
|
||||
StorybookModule,
|
||||
CoreComponentsModule,
|
||||
CoreSearchComponentsModule,
|
||||
],
|
||||
})
|
||||
export class CoreSearchComponentsStorybookModule {}
|
|
@ -0,0 +1,18 @@
|
|||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h1>Search Results</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="limited-width">
|
||||
<div>
|
||||
<core-search-box></core-search-box>
|
||||
<ion-list>
|
||||
<core-search-global-search-result *ngFor="let result of results" [result]="result" (onClick)="resultClicked(result.title)">
|
||||
</core-search-global-search-result>
|
||||
</ion-list>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
|
@ -0,0 +1,121 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { CoreCourseListItem } from '@features/courses/services/courses';
|
||||
import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar';
|
||||
import { CoreSearchGlobalSearchResult } from '@features/search/services/global-search';
|
||||
import courses from '@/assets/storybook/courses.json';
|
||||
|
||||
@Component({
|
||||
selector: 'core-search-global-search-results-page',
|
||||
templateUrl: 'global-search-results-page.html',
|
||||
})
|
||||
export class CoreSearchGlobalSearchResultsPageComponent {
|
||||
|
||||
results: CoreSearchGlobalSearchResult[] = [
|
||||
{
|
||||
id: 1,
|
||||
url: '',
|
||||
title: 'Activity forum test',
|
||||
content: 'this is a content test for a forum to see in the search result.',
|
||||
context: {
|
||||
courseName: 'Course 102',
|
||||
userName: 'Stephania Krovalenko',
|
||||
},
|
||||
module: {
|
||||
name: 'forum',
|
||||
iconurl: 'assets/img/mod/forum.svg',
|
||||
area: 'activity',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
url: '',
|
||||
title: 'Activity assignment test',
|
||||
content: 'this is a content test for a forum to see in the search result.',
|
||||
context: {
|
||||
courseName: 'Course 102',
|
||||
},
|
||||
module: {
|
||||
name: 'assign',
|
||||
iconurl: 'assets/img/mod/assign.svg',
|
||||
area: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
url: '',
|
||||
title: 'Course 101',
|
||||
course: courses[0] as CoreCourseListItem,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: '',
|
||||
title: 'John the Tester',
|
||||
user: {
|
||||
fullname: 'John Doe',
|
||||
profileimageurl: 'https://placekitten.com/300/300',
|
||||
} as CoreUserWithAvatar,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: '',
|
||||
title: 'Search result title',
|
||||
content: 'this is a content test for a forum to see in the search result.',
|
||||
context: {
|
||||
userName: 'Stephania Krovalenko',
|
||||
},
|
||||
module: {
|
||||
name: 'forum',
|
||||
iconurl: 'assets/img/mod/forum.svg',
|
||||
area: 'post',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
url: '',
|
||||
title: 'Side block',
|
||||
context: {
|
||||
courseName: 'Moodle Site',
|
||||
},
|
||||
component: {
|
||||
name: 'block_html',
|
||||
iconurl: 'https://master.mm.moodledemo.net/theme/image.php?theme=boost&component=core&image=e%2Fanchor',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
url: '',
|
||||
title: 'Course section',
|
||||
context: {
|
||||
courseName: 'Course 101',
|
||||
},
|
||||
component: {
|
||||
name: 'core_course',
|
||||
iconurl: 'https://master.mm.moodledemo.net/theme/image.php?theme=boost&component=core&image=i%2Fsection',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Result clicked.
|
||||
*
|
||||
* @param title Result title.
|
||||
*/
|
||||
resultClicked(title: string): void {
|
||||
alert(`clicked on ${title}`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Meta, moduleMetadata } from '@storybook/angular';
|
||||
|
||||
import { story } from '@/storybook/utils/helpers';
|
||||
|
||||
import { CoreSearchGlobalSearchResultComponent } from '@features/search/components/global-search-result/global-search-result';
|
||||
import { CoreSearchComponentsStorybookModule } from '@features/search/stories/components/components.module';
|
||||
import {
|
||||
CoreSearchGlobalSearchResultsPageComponent,
|
||||
} from '@features/search/stories/components/global-search-results-page/global-search-results-page';
|
||||
import { APP_INITIALIZER } from '@angular/core';
|
||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||
import { AddonModForumModuleHandler } from '@addons/mod/forum/services/handlers/module';
|
||||
import { AddonModAssignModuleHandler } from '@addons/mod/assign/services/handlers/module';
|
||||
import { CoreSearchGlobalSearchResult } from '@features/search/services/global-search';
|
||||
import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar';
|
||||
import { CoreCourseListItem } from '@features/courses/services/courses';
|
||||
import courses from '@/assets/storybook/courses.json';
|
||||
|
||||
interface Args {
|
||||
title: string;
|
||||
content: string;
|
||||
image: 'course' | 'user' | 'none';
|
||||
module: 'forum-activity' | 'forum-post' | 'assign' | 'none';
|
||||
courseContext: boolean;
|
||||
userContext: boolean;
|
||||
}
|
||||
|
||||
export default <Meta<Args>> {
|
||||
title: 'Core/Search/Global Search Result',
|
||||
component: CoreSearchGlobalSearchResultComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [CoreSearchComponentsStorybookModule],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue() {
|
||||
CoreCourseModuleDelegate.registerHandler(AddonModForumModuleHandler.instance);
|
||||
CoreCourseModuleDelegate.registerHandler(AddonModAssignModuleHandler.instance);
|
||||
CoreCourseModuleDelegate.updateHandlers();
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
image: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['course', 'user', 'none'],
|
||||
},
|
||||
},
|
||||
module: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['forum-activity', 'forum-post', 'assign', 'none'],
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
title: 'Result #1',
|
||||
content: 'This item seems really interesting, maybe you should click through',
|
||||
image: 'none',
|
||||
module: 'none',
|
||||
courseContext: false,
|
||||
userContext: false,
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/h3E7pkfgyImJPaYmTfnwuF/Global-Search?node-id=118%3A4610',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = story<Args>(({ image, courseContext, userContext, module, ...args }) => {
|
||||
const result: CoreSearchGlobalSearchResult = {
|
||||
...args,
|
||||
id: 1,
|
||||
url: '',
|
||||
};
|
||||
|
||||
if (courseContext || userContext) {
|
||||
result.context = {
|
||||
courseName: courseContext ? 'Course 101' : undefined,
|
||||
userName: userContext ? 'John Doe' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (module !== 'none') {
|
||||
const name = module.startsWith('forum') ? 'forum' : module;
|
||||
|
||||
result.module = {
|
||||
name,
|
||||
iconurl: `assets/img/mod/${name}.svg`,
|
||||
area: module.startsWith('forum') ? module.substring(6) : '',
|
||||
};
|
||||
}
|
||||
|
||||
switch (image) {
|
||||
case 'course':
|
||||
result.course = courses[0] as CoreCourseListItem;
|
||||
break;
|
||||
case 'user':
|
||||
result.user = {
|
||||
fullname: 'John Doe',
|
||||
profileimageurl: 'https://placekitten.com/300/300',
|
||||
} as CoreUserWithAvatar;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
component: CoreSearchGlobalSearchResultComponent,
|
||||
props: { result },
|
||||
};
|
||||
});
|
||||
|
||||
export const Primary = story<Args>(Template);
|
||||
export const ResultsPage = story<Args>(() => ({ component: CoreSearchGlobalSearchResultsPageComponent }));
|
|
@ -0,0 +1,147 @@
|
|||
@core @core_search @app @javascript @lms_from4.3
|
||||
Feature: Test Global Search
|
||||
|
||||
Background:
|
||||
Given solr is installed
|
||||
And the following config values are set as admin:
|
||||
| enableglobalsearch | 1 |
|
||||
| searchengine | solr |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
| Course 2 | C2 |
|
||||
And the following "users" exist:
|
||||
| username |
|
||||
| student1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| page | Test page 01 | C1 | page01 |
|
||||
| page | Test page 02 | C1 | page02 |
|
||||
| page | Test page 03 | C1 | page03 |
|
||||
| page | Test page 04 | C1 | page04 |
|
||||
| page | Test page 05 | C1 | page05 |
|
||||
| page | Test page 06 | C1 | page06 |
|
||||
| page | Test page 07 | C1 | page07 |
|
||||
| page | Test page 08 | C1 | page08 |
|
||||
| page | Test page 09 | C1 | page09 |
|
||||
| page | Test page 10 | C1 | page10 |
|
||||
| page | Test page 11 | C1 | page11 |
|
||||
| page | Test page 12 | C1 | page12 |
|
||||
| page | Test page 13 | C1 | page13 |
|
||||
| page | Test page 14 | C1 | page14 |
|
||||
| page | Test page 15 | C1 | page15 |
|
||||
| page | Test page 16 | C1 | page16 |
|
||||
| page | Test page 17 | C1 | page17 |
|
||||
| page | Test page 18 | C1 | page18 |
|
||||
| page | Test page 19 | C1 | page19 |
|
||||
| page | Test page 20 | C1 | page20 |
|
||||
| page | Test page 21 | C1 | page21 |
|
||||
| page | Test page C2 | C2 | pagec2 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber |
|
||||
| forum | Test forum | Test forum intro | C1 | forum |
|
||||
|
||||
Scenario: Search in a site
|
||||
Given global search expects the query "page" and will return:
|
||||
| type | idnumber |
|
||||
| activity | page01 |
|
||||
| activity | page02 |
|
||||
| activity | page03 |
|
||||
| activity | page04 |
|
||||
| activity | page05 |
|
||||
| activity | page06 |
|
||||
| activity | page07 |
|
||||
| activity | page08 |
|
||||
| activity | page09 |
|
||||
| activity | page10 |
|
||||
| activity | page11 |
|
||||
| activity | page12 |
|
||||
| activity | pagec2 |
|
||||
And I entered the app as "student1"
|
||||
When I press the more menu button in the app
|
||||
And I press "Global search" in the app
|
||||
And I set the field "Search" to "page" in the app
|
||||
And I press "Search" "button" in the app
|
||||
Then I should find "Test page 01" in the app
|
||||
And I should find "Test page 10" in the app
|
||||
|
||||
When I load more items in the app
|
||||
Then I should find "Test page 11" in the app
|
||||
|
||||
When I press "Test page 01" in the app
|
||||
Then I should find "Test page content" in the app
|
||||
|
||||
When I press the back button in the app
|
||||
And global search expects the query "forum" and will return:
|
||||
| type | idnumber |
|
||||
| activity | forum |
|
||||
And I set the field "Search" to "forum" in the app
|
||||
And I press "Search" "button" in the app
|
||||
Then I should find "Test forum" in the app
|
||||
But I should not find "Test page" in the app
|
||||
|
||||
When I press "Test forum" in the app
|
||||
Then I should find "Test forum intro" in the app
|
||||
|
||||
When I press the back button in the app
|
||||
And I press "Clear search" in the app
|
||||
Then I should find "What are you searching for?" in the app
|
||||
But I should not find "Test forum" in the app
|
||||
|
||||
Given global search expects the query "noresults" and will return:
|
||||
| type | idnumber |
|
||||
And I set the field "Search" to "noresults" in the app
|
||||
And I press "Search" "button" in the app
|
||||
Then I should find "No results for" in the app
|
||||
|
||||
# TODO test other results like course, user, and messages (global search generator not supported)
|
||||
|
||||
Scenario: Filter results
|
||||
Given global search expects the query "page" and will return:
|
||||
| type | idnumber |
|
||||
| activity | page01 |
|
||||
And I entered the app as "student1"
|
||||
When I press the more menu button in the app
|
||||
And I press "Global search" in the app
|
||||
And I set the field "Search" to "page" in the app
|
||||
And I press "Search" "button" in the app
|
||||
Then I should find "Test page 01" in the app
|
||||
|
||||
When I press "Filter" in the app
|
||||
And I press "C1" in the app
|
||||
And I press "Users" in the app
|
||||
And global search expects the query "page" and will return:
|
||||
| type | idnumber |
|
||||
| activity | page02 |
|
||||
And I press "Close" in the app
|
||||
Then I should find "Test page 02" in the app
|
||||
But I should not find "Test page 01" in the app
|
||||
|
||||
Scenario: See search banner
|
||||
Given the following config values are set as admin:
|
||||
| searchbannerenable | 1 |
|
||||
| searchbanner | Search indexing is under maintentance! |
|
||||
And I entered the app as "student1"
|
||||
When I press the more menu button in the app
|
||||
And I press "Global search" in the app
|
||||
Then I should find "Search indexing is under maintentance!" in the app
|
||||
|
||||
Scenario: Open from side block
|
||||
Given global search expects the query "message" and will return:
|
||||
| type | idnumber |
|
||||
| activity | page01 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference |
|
||||
| globalsearch | Course | C1 |
|
||||
And I entered the course "Course 1" as "student1" in the app
|
||||
When I press "Open block drawer" in the app
|
||||
And I press "Global search" in the app
|
||||
Then I should find "What are you searching for?" in the app
|
||||
|
||||
When I press "Filter" in the app
|
||||
Then "C1" should be selected in the app
|
||||
But "C2" should not be selected in the app
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { Params } from '@angular/router';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
|
||||
import { CoreSite, CoreSiteConfig } from '@classes/site';
|
||||
import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course';
|
||||
|
@ -31,6 +31,7 @@ import { CoreBlockHelper } from '@features/block/services/block-helper';
|
|||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreTime } from '@singletons/time';
|
||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||
import { CoreBlockSideBlocksComponent } from '@features/block/components/side-blocks/side-blocks';
|
||||
|
||||
/**
|
||||
* Page that displays site home index.
|
||||
|
@ -58,7 +59,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
|||
protected updateSiteObserver: CoreEventObserver;
|
||||
protected logView: () => void;
|
||||
|
||||
constructor() {
|
||||
constructor(protected route: ActivatedRoute) {
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
||||
|
@ -102,6 +103,10 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
|||
this.loadContent().finally(() => {
|
||||
this.dataLoaded = true;
|
||||
});
|
||||
|
||||
this.openFocusedInstance();
|
||||
|
||||
this.route.queryParams.subscribe(() => this.openFocusedInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -226,4 +231,22 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
|||
this.updateSiteObserver.off();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there is a focused instance in the page parameters and open it.
|
||||
*/
|
||||
private openFocusedInstance() {
|
||||
const blockInstanceId = CoreNavigator.getRouteNumberParam('blockInstanceId');
|
||||
|
||||
if (blockInstanceId) {
|
||||
CoreDomUtils.openSideModal({
|
||||
component: CoreBlockSideBlocksComponent,
|
||||
componentProps: {
|
||||
contextLevel: 'course',
|
||||
instanceId: this.siteHomeId,
|
||||
initialBlockInstanceId: blockInstanceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { makeSingleton } from '@singletons';
|
|||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSiteHomeHomeHandlerService } from './sitehome-home';
|
||||
import { CoreMainMenuHomeHandlerService } from '@features/mainmenu/services/handlers/mainmenu';
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Handler to treat links to site home index.
|
||||
|
@ -36,7 +37,14 @@ export class CoreSiteHomeIndexLinkHandlerService extends CoreContentLinksHandler
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getActions(): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
getActions(siteIds: string[], url: string): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
const pageParams: Params = {};
|
||||
const matches = url.match(/#inst(\d+)/);
|
||||
|
||||
if (matches && matches[1]) {
|
||||
pageParams.blockInstanceId = parseInt(matches[1], 10);
|
||||
}
|
||||
|
||||
return [{
|
||||
action: (siteId: string): void => {
|
||||
CoreNavigator.navigateToSitePath(
|
||||
|
@ -44,6 +52,7 @@ export class CoreSiteHomeIndexLinkHandlerService extends CoreContentLinksHandler
|
|||
{
|
||||
preferCurrentTab: false,
|
||||
siteId,
|
||||
params: pageParams,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('Site Home link handlers', () => {
|
|||
expect(CoreNavigator.navigateToSitePath).toHaveBeenCalledWith('/home/site', {
|
||||
siteId,
|
||||
preferCurrentTab: false,
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ import { CoreNetwork } from '@services/network';
|
|||
import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config';
|
||||
import { CoreLang, CoreLangFormat } from '@services/lang';
|
||||
import { CoreNative } from '@features/native/services/native';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
|
||||
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
|
||||
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id';
|
||||
|
@ -707,6 +708,39 @@ export class CoreSitesProvider {
|
|||
return CoreConstants.CONFIG.wsservice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit a site link.
|
||||
*
|
||||
* @param url URL to handle.
|
||||
* @param options Behaviour options.
|
||||
* @param options.siteId Site Id.
|
||||
* @param options.username Username related with the URL. E.g. in 'http://myuser@m.com', url would be 'http://m.com' and
|
||||
* the username 'myuser'. Don't use it if you don't want to filter by username.
|
||||
* @param options.checkRoot Whether to check if the URL is the root URL of a site.
|
||||
* @param options.openBrowserRoot Whether to open in browser if it's root URL and it belongs to current site.
|
||||
*/
|
||||
async visitLink(
|
||||
url: string,
|
||||
options: {
|
||||
siteId?: string;
|
||||
username?: string;
|
||||
checkRoot?: boolean;
|
||||
openBrowserRoot?: boolean;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const treated = await CoreContentLinksHelper.handleLink(url, options.username, options.checkRoot, options.openBrowserRoot);
|
||||
|
||||
if (treated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const site = options.siteId
|
||||
? await CoreSites.getSite(options.siteId)
|
||||
: CoreSites.getCurrentSite();
|
||||
|
||||
await site?.openInBrowserWithAutoLogin(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for the minimum required version.
|
||||
*
|
||||
|
|
|
@ -1506,6 +1506,28 @@ export class CoreDomUtilsProvider {
|
|||
return loading;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a loading modal whilst an operation is running, and an error modal if it fails.
|
||||
*
|
||||
* @param text Loading dialog text.
|
||||
* @param needsTranslate Whether the 'text' needs to be translated.
|
||||
* @param operation Operation.
|
||||
* @returns Operation result.
|
||||
*/
|
||||
async showOperationModals<T>(text: string, needsTranslate: boolean, operation: () => Promise<T>): Promise<T | null> {
|
||||
const modal = await this.showModalLoading(text, needsTranslate);
|
||||
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a modal warning the user that he should use a different app.
|
||||
*
|
||||
|
|
|
@ -20,6 +20,14 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
|||
import englishTranslations from '@/assets/lang/en.json';
|
||||
import { CoreApplicationInitStatus } from '@classes/application-init-status';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreSitesProviderStub, CoreSitesStub } from '@/storybook/stubs/services/sites';
|
||||
import { CoreSitesProvider } from '@services/sites';
|
||||
import { CoreDbProviderStub } from '@/storybook/stubs/services/db';
|
||||
import { CoreDbProvider } from '@services/db';
|
||||
import { CoreFilepoolProviderStub } from '@/storybook/stubs/services/filepool';
|
||||
import { CoreFilepoolProvider } from '@services/filepool';
|
||||
import { HttpClientStub } from '@/storybook/stubs/services/http';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
// For translate loader. AoT requires an exported function for factories.
|
||||
export class StaticTranslateLoader extends TranslateLoader {
|
||||
|
@ -45,12 +53,17 @@ export class StaticTranslateLoader extends TranslateLoader {
|
|||
],
|
||||
providers: [
|
||||
{ provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus },
|
||||
{ provide: CoreSitesProvider, useClass: CoreSitesProviderStub },
|
||||
{ provide: CoreDbProvider, useClass: CoreDbProviderStub },
|
||||
{ provide: CoreFilepoolProvider, useClass: CoreFilepoolProviderStub },
|
||||
{ provide: HttpClient, useClass: HttpClientStub },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
Translate.setDefaultLang('en');
|
||||
Translate.use('en');
|
||||
CoreSitesStub.stubCurrentSite();
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreSite, CoreSiteConfigResponse, CoreSiteInfo, CoreSiteWSPreSets, WSObservable } from '@classes/site';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
export interface CoreSiteFixture {
|
||||
id: string;
|
||||
info: CoreSiteInfo;
|
||||
}
|
||||
|
||||
export class CoreSiteStub extends CoreSite {
|
||||
|
||||
protected wsStubs: Record<string, unknown> = {};
|
||||
|
||||
constructor (fixture: CoreSiteFixture) {
|
||||
super(fixture.id, fixture.info.siteurl, undefined, fixture.info);
|
||||
|
||||
this.stubWSResponse<CoreSiteConfigResponse>('tool_mobile_get_config', {
|
||||
settings: [],
|
||||
warnings: [],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
readObservable<T = unknown>(wsFunction: string, data: unknown, preSets?: CoreSiteWSPreSets): WSObservable<T> {
|
||||
if (wsFunction in this.wsStubs) {
|
||||
return of(this.wsStubs[wsFunction] as T);
|
||||
}
|
||||
|
||||
return super.readObservable<T>(wsFunction, data, preSets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare as stubbed response for a given WS.
|
||||
*
|
||||
* @param wsFunction WS function.
|
||||
* @param response Response.
|
||||
*/
|
||||
stubWSResponse<T=unknown>(wsFunction: string, response: T): void {
|
||||
this.wsStubs[wsFunction] = response;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { SQLiteDB } from '@classes/sqlitedb';
|
||||
import { SQLiteObject } from '@ionic-native/sqlite/ngx';
|
||||
|
||||
/**
|
||||
* SQlite database stub.
|
||||
*/
|
||||
export class SQLiteDBStub extends SQLiteDB {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async createDatabase(): Promise<SQLiteObject> {
|
||||
return new Proxy({
|
||||
executeSql: () => Promise.resolve({ insertId: Math.random().toString() }),
|
||||
}, {}) as unknown as SQLiteObject;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { SQLiteDBStub } from '@/storybook/stubs/classes/sqlitedb';
|
||||
import { SQLiteDB } from '@classes/sqlitedb';
|
||||
import { CoreDbProvider } from '@services/db';
|
||||
|
||||
/**
|
||||
* Database provider stub.
|
||||
*/
|
||||
export class CoreDbProviderStub extends CoreDbProvider {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDB(name: string, forceNew?: boolean): SQLiteDB {
|
||||
if (this.dbInstances[name] === undefined || forceNew) {
|
||||
this.dbInstances[name] = new SQLiteDBStub(name);
|
||||
}
|
||||
|
||||
return this.dbInstances[name];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreFilepoolProvider } from '@services/filepool';
|
||||
|
||||
/**
|
||||
* Filepool provider stub.
|
||||
*/
|
||||
export class CoreFilepoolProviderStub extends CoreFilepoolProvider {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async getSrcByUrl(siteId: string, fileUrl: string): Promise<string> {
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreFilepoolStub = makeSingleton<CoreFilepoolProviderStub>(CoreFilepoolProvider);
|
|
@ -0,0 +1,38 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { HttpClient, HttpHandler } from '@angular/common/http';
|
||||
import { from, Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Http service stub.
|
||||
*/
|
||||
export class HttpClientStub extends HttpClient {
|
||||
|
||||
constructor() {
|
||||
super(null as unknown as HttpHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get(url: string): Observable<any> {
|
||||
return from(fetch(url).then(response => response.text()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const HttpStub = makeSingleton<HttpClientStub>(HttpClient);
|
|
@ -0,0 +1,43 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import school from '@/assets/storybook/sites/school.json';
|
||||
import { CoreSiteFixture, CoreSiteStub } from '@/storybook/stubs/classes/site';
|
||||
import { CoreSitesProvider } from '@services/sites';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
* Sites provider stub.
|
||||
*/
|
||||
export class CoreSitesProviderStub extends CoreSitesProvider {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getRequiredCurrentSite!: () => CoreSiteStub;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
stubCurrentSite(fixture?: CoreSiteFixture): CoreSiteStub {
|
||||
if (!this.currentSite) {
|
||||
this.currentSite = new CoreSiteStub(fixture ?? school);
|
||||
}
|
||||
|
||||
return this.getRequiredCurrentSite();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreSitesStub = makeSingleton<CoreSitesProviderStub>(CoreSitesProvider);
|
Loading…
Reference in New Issue