commit
cbfc866af4
|
@ -1,5 +1,11 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
framework: '@storybook/angular',
|
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'],
|
stories: ['../src/**/*.stories.ts'],
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,9 @@ import '!style-loader!css-loader!sass-loader!./styles.scss';
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
layout: 'centered',
|
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 {
|
.core-error-info {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"",
|
"",
|
||||||
"@Component({",
|
"@Component({",
|
||||||
" selector: '$2${TM_FILENAME_BASE}',",
|
" selector: '$2${TM_FILENAME_BASE}',",
|
||||||
" templateUrl: '$2${TM_FILENAME_BASE}.html',",
|
" templateUrl: '${TM_FILENAME_BASE}.html',",
|
||||||
"})",
|
"})",
|
||||||
"export class ${1:${TM_FILENAME_BASE}}Component {",
|
"export class ${1:${TM_FILENAME_BASE}}Component {",
|
||||||
"",
|
"",
|
||||||
|
@ -110,6 +110,24 @@
|
||||||
],
|
],
|
||||||
"description": "[Moodle] Create a Pure Singleton"
|
"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": {
|
"Innherit doc": {
|
||||||
"prefix": "inheritdoc",
|
"prefix": "inheritdoc",
|
||||||
"body": [
|
"body": [
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -149,9 +149,11 @@
|
||||||
"@ionic/angular-toolkit": "^2.3.3",
|
"@ionic/angular-toolkit": "^2.3.3",
|
||||||
"@ionic/cli": "^6.19.0",
|
"@ionic/cli": "^6.19.0",
|
||||||
"@storybook/addon-controls": "~6.1.21",
|
"@storybook/addon-controls": "~6.1.21",
|
||||||
|
"@storybook/addon-viewport": "~6.1.21",
|
||||||
"@storybook/angular": "~6.1.21",
|
"@storybook/angular": "~6.1.21",
|
||||||
"@types/faker": "^5.1.3",
|
"@types/faker": "^5.1.3",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
|
"@types/marked": "^4.3.1",
|
||||||
"@types/node": "^12.12.64",
|
"@types/node": "^12.12.64",
|
||||||
"@types/resize-observer-browser": "^0.1.5",
|
"@types/resize-observer-browser": "^0.1.5",
|
||||||
"@types/webpack-env": "^1.16.0",
|
"@types/webpack-env": "^1.16.0",
|
||||||
|
@ -183,9 +185,13 @@
|
||||||
"jest-preset-angular": "^8.3.1",
|
"jest-preset-angular": "^8.3.1",
|
||||||
"jest-raw-loader": "^1.0.1",
|
"jest-raw-loader": "^1.0.1",
|
||||||
"jsonc-parser": "^2.3.1",
|
"jsonc-parser": "^2.3.1",
|
||||||
|
"marked": "^4.3.0",
|
||||||
"minimatch": "^5.1.0",
|
"minimatch": "^5.1.0",
|
||||||
"native-run": "^1.4.0",
|
"native-run": "^1.4.0",
|
||||||
"patch-package": "^6.5.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",
|
"terser-webpack-plugin": "^4.2.3",
|
||||||
"ts-jest": "^26.4.1",
|
"ts-jest": "^26.4.1",
|
||||||
"ts-node": "~8.3.0",
|
"ts-node": "~8.3.0",
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
"addon.block_calendarupcoming.pluginname": "block_calendar_upcoming",
|
"addon.block_calendarupcoming.pluginname": "block_calendar_upcoming",
|
||||||
"addon.block_comments.pluginname": "block_comments",
|
"addon.block_comments.pluginname": "block_comments",
|
||||||
"addon.block_completionstatus.pluginname": "block_completionstatus",
|
"addon.block_completionstatus.pluginname": "block_completionstatus",
|
||||||
|
"addon.block_globalsearch.pluginname": "block_globalsearch",
|
||||||
"addon.block_glossaryrandom.pluginname": "block_glossary_random",
|
"addon.block_glossaryrandom.pluginname": "block_glossary_random",
|
||||||
"addon.block_learningplans.pluginname": "block_lp",
|
"addon.block_learningplans.pluginname": "block_lp",
|
||||||
"addon.block_myoverview.all": "block_myoverview",
|
"addon.block_myoverview.all": "block_myoverview",
|
||||||
|
@ -2310,6 +2311,16 @@
|
||||||
"core.scanqr": "local_moodlemobileapp",
|
"core.scanqr": "local_moodlemobileapp",
|
||||||
"core.scrollbackward": "local_moodlemobileapp",
|
"core.scrollbackward": "local_moodlemobileapp",
|
||||||
"core.scrollforward": "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.search": "moodle",
|
||||||
"core.searching": "local_moodlemobileapp",
|
"core.searching": "local_moodlemobileapp",
|
||||||
"core.searchresults": "moodle",
|
"core.searchresults": "moodle",
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { AddonBlockSiteMainMenuModule } from './sitemainmenu/sitemainmenu.module
|
||||||
import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses.module';
|
import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses.module';
|
||||||
import { AddonBlockTagsModule } from './tags/tags.module';
|
import { AddonBlockTagsModule } from './tags/tags.module';
|
||||||
import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
||||||
|
import { AddonBlockGlobalSearchModule } from '@addons/block/globalsearch/globalsearch.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -55,6 +56,7 @@ import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
||||||
AddonBlockCommentsModule,
|
AddonBlockCommentsModule,
|
||||||
AddonBlockCompletionStatusModule,
|
AddonBlockCompletionStatusModule,
|
||||||
AddonBlockCourseListModule,
|
AddonBlockCourseListModule,
|
||||||
|
AddonBlockGlobalSearchModule,
|
||||||
AddonBlockGlossaryRandomModule,
|
AddonBlockGlossaryRandomModule,
|
||||||
AddonBlockHtmlModule,
|
AddonBlockHtmlModule,
|
||||||
AddonBlockLearningPlansModule,
|
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';
|
} from '../../services/recentlyaccesseditems';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,10 +87,7 @@ export class AddonBlockRecentlyAccessedItemsComponent extends CoreBlockBaseCompo
|
||||||
const modal = await CoreDomUtils.showModalLoading();
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const treated = await CoreContentLinksHelper.handleLink(url);
|
await CoreSites.visitLink(url);
|
||||||
if (!treated) {
|
|
||||||
return CoreSites.getCurrentSite()?.openInBrowserWithAutoLogin(url);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
|
||||||
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||||
import { AddonBlockTimelineDayEvents } from '@addons/block/timeline/classes/section';
|
import { AddonBlockTimelineDayEvents } from '@addons/block/timeline/classes/section';
|
||||||
|
|
||||||
|
@ -54,10 +53,7 @@ export class AddonBlockTimelineEventsComponent {
|
||||||
const modal = await CoreDomUtils.showModalLoading();
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const treated = await CoreContentLinksHelper.handleLink(url);
|
await CoreSites.visitLink(url);
|
||||||
if (!treated) {
|
|
||||||
return CoreSites.getRequiredCurrentSite().openInBrowserWithAutoLogin(url);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
|
|
||||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
|
||||||
import { CoreCourse, CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
import { CoreCourse, CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
|
@ -428,11 +427,7 @@ export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave {
|
||||||
const modal = await CoreDomUtils.showModalLoading();
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const treated = await CoreContentLinksHelper.handleLink(this.siteAfterSubmit);
|
await CoreSites.visitLink(this.siteAfterSubmit, { siteId: this.currentSite.id });
|
||||||
|
|
||||||
if (!treated) {
|
|
||||||
await this.currentSite.openInBrowserWithAutoLogin(this.siteAfterSubmit);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||||
import { CoreConstants, ModPurpose } from '@/core/constants';
|
import { CoreConstants, ModPurpose } from '@/core/constants';
|
||||||
import { AddonModForumIndexComponent } from '../../components/index';
|
|
||||||
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
|
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
|
||||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||||
import { CoreIonicColorNames } from '@singletons/colors';
|
import { CoreIonicColorNames } from '@singletons/colors';
|
||||||
|
@ -86,6 +85,8 @@ export class AddonModForumModuleHandlerService extends CoreModuleHandlerBase imp
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async getMainComponent(): Promise<Type<unknown> | undefined> {
|
async getMainComponent(): Promise<Type<unknown> | undefined> {
|
||||||
|
const { AddonModForumIndexComponent } = await import('../../components/index');
|
||||||
|
|
||||||
return AddonModForumIndexComponent;
|
return AddonModForumIndexComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
@ -33,11 +32,10 @@ export class AddonModUrlHelperProvider {
|
||||||
const modal = await CoreDomUtils.showModalLoading();
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const treated = await CoreContentLinksHelper.handleLink(url, undefined, true, true);
|
await CoreSites.visitLink(url, {
|
||||||
|
checkRoot: true,
|
||||||
if (!treated) {
|
openBrowserRoot: true,
|
||||||
await CoreSites.getCurrentSite()?.openInBrowserWithAutoLogin(url);
|
});
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
@ -75,13 +74,7 @@ export class AddonReportInsightsActionLinkHandlerService extends CoreContentLink
|
||||||
// Try to open the link in the app.
|
// Try to open the link in the app.
|
||||||
const forwardUrl = decodeURIComponent(params.forwardurl);
|
const forwardUrl = decodeURIComponent(params.forwardurl);
|
||||||
|
|
||||||
const treated = await CoreContentLinksHelper.handleLink(forwardUrl);
|
await CoreSites.visitLink(forwardUrl, { siteId });
|
||||||
if (!treated) {
|
|
||||||
// Cannot be opened in the app, open in browser.
|
|
||||||
const site = await CoreSites.getSite(siteId);
|
|
||||||
|
|
||||||
await site.openInBrowserWithAutoLogin(forwardUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
|
|
|
@ -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);
|
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.
|
* Check whether data is loaded.
|
||||||
*
|
*
|
||||||
|
@ -88,6 +97,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.items = null;
|
this.items = null;
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
|
this.loaded = false;
|
||||||
|
|
||||||
this.listeners.forEach(listener => listener.onReset?.call(listener));
|
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.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreItemsManagerSource } from './items-manager-source';
|
import { CorePaginatedItemsManagerSource } from './paginated-items-manager-source';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routed items collection source data.
|
* Routed items collection source data.
|
||||||
*/
|
*/
|
||||||
export abstract class CoreRoutedItemsManagerSource<Item = unknown> extends CoreItemsManagerSource<Item> {
|
export abstract class CoreRoutedItemsManagerSource<Item = unknown> extends CorePaginatedItemsManagerSource<Item> {
|
||||||
|
|
||||||
protected hasMoreItems = true;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a string to identify instances constructed with the given arguments as being reusable.
|
* 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('-');
|
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.
|
* 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> & {
|
export type CoreSiteConfig = Record<string, string> & {
|
||||||
supportavailability?: string; // String representation of CoreSiteConfigSupportAvailability.
|
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 { CoreGroupSelectorComponent } from './group-selector/group-selector';
|
||||||
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
|
import { CoreRefreshButtonModalComponent } from './refresh-button-modal/refresh-button-modal';
|
||||||
import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
||||||
|
import { CoreCourseImageComponent } from '@components/course-image/course-image';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -75,6 +76,7 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
||||||
CoreContextMenuComponent,
|
CoreContextMenuComponent,
|
||||||
CoreContextMenuItemComponent,
|
CoreContextMenuItemComponent,
|
||||||
CoreContextMenuPopoverComponent,
|
CoreContextMenuPopoverComponent,
|
||||||
|
CoreCourseImageComponent,
|
||||||
CoreDownloadRefreshComponent,
|
CoreDownloadRefreshComponent,
|
||||||
CoreDynamicComponent,
|
CoreDynamicComponent,
|
||||||
CoreEmptyBoxComponent,
|
CoreEmptyBoxComponent,
|
||||||
|
@ -128,6 +130,7 @@ import { CoreSheetModalComponent } from '@components/sheet-modal/sheet-modal';
|
||||||
CoreContextMenuComponent,
|
CoreContextMenuComponent,
|
||||||
CoreContextMenuItemComponent,
|
CoreContextMenuItemComponent,
|
||||||
CoreContextMenuPopoverComponent,
|
CoreContextMenuPopoverComponent,
|
||||||
|
CoreCourseImageComponent,
|
||||||
CoreDownloadRefreshComponent,
|
CoreDownloadRefreshComponent,
|
||||||
CoreDynamicComponent,
|
CoreDynamicComponent,
|
||||||
CoreEmptyBoxComponent,
|
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";
|
@import "~theme/globals";
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
|
--image-size: 120px;
|
||||||
|
--icon-color: var(--text-color);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
--image-size: 120px;
|
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: var(--image-size);
|
font-size: var(--image-size);
|
||||||
|
color: var(--icon-color);
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
height: var(--image-size);
|
height: var(--image-size);
|
||||||
|
@ -28,6 +29,20 @@
|
||||||
&.core-empty-box-clickable {
|
&.core-empty-box-clickable {
|
||||||
z-index: 0;
|
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) {
|
@include media-breakpoint-down(sm) {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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.
|
* 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 {
|
export class CoreEmptyBoxComponent {
|
||||||
|
|
||||||
@Input() message = ''; // Message to display.
|
@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() icon?: string; // Name of the icon to use.
|
||||||
@Input() image?: string; // Image source. If an icon is provided, image won't be used.
|
@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.
|
@Input() flipIconRtl = false; // Whether to flip the icon in RTL. Defaults to false.
|
||||||
|
@ -39,4 +40,9 @@ export class CoreEmptyBoxComponent {
|
||||||
*/
|
*/
|
||||||
@Input() inline = false;
|
@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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { OnInit, Component } from '@angular/core';
|
import { OnInit, Component, HostBinding } from '@angular/core';
|
||||||
import { CoreBlockBaseComponent } from '../../classes/base-block-component';
|
import { CoreBlockBaseComponent } from '../../classes/base-block-component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,6 +26,8 @@ export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implem
|
||||||
|
|
||||||
courseId?: number;
|
courseId?: number;
|
||||||
|
|
||||||
|
@HostBinding('attr.id') id?: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('CoreBlockPreRenderedComponent');
|
super('CoreBlockPreRenderedComponent');
|
||||||
}
|
}
|
||||||
|
@ -39,6 +41,8 @@ export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implem
|
||||||
this.courseId = this.contextLevel == 'course' ? this.instanceId : undefined;
|
this.courseId = this.contextLevel == 'course' ? this.instanceId : undefined;
|
||||||
|
|
||||||
this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.';
|
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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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 { ModalController } from '@singletons';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
|
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
|
||||||
|
@ -22,6 +22,7 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreCoursesDashboard } from '@features/courses/services/dashboard';
|
import { CoreCoursesDashboard } from '@features/courses/services/dashboard';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreDom } from '@singletons/dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays the list of side blocks.
|
* Component that displays the list of side blocks.
|
||||||
|
@ -35,6 +36,7 @@ export class CoreBlockSideBlocksComponent implements OnInit {
|
||||||
|
|
||||||
@Input() contextLevel!: string;
|
@Input() contextLevel!: string;
|
||||||
@Input() instanceId!: number;
|
@Input() instanceId!: number;
|
||||||
|
@Input() initialBlockInstanceId?: number;
|
||||||
@Input() myDashboardPage?: string;
|
@Input() myDashboardPage?: string;
|
||||||
|
|
||||||
@ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
|
@ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
|
||||||
|
@ -42,12 +44,16 @@ export class CoreBlockSideBlocksComponent implements OnInit {
|
||||||
loaded = false;
|
loaded = false;
|
||||||
blocks: CoreCourseBlock[] = [];
|
blocks: CoreCourseBlock[] = [];
|
||||||
|
|
||||||
|
constructor(protected elementRef: ElementRef<HTMLElement>) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
this.loadContent().finally(() => {
|
this.loadContent().finally(() => {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
|
||||||
|
this.focusInitialBlock();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,4 +125,20 @@ export class CoreBlockSideBlocksComponent implements OnInit {
|
||||||
ModalController.dismiss();
|
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 { CoreUserTourDirectiveOptions } from '@directives/user-tour';
|
||||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||||
import { CorePlatform } from '@services/platform';
|
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.
|
* 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() sections: CoreCourseSectionToDisplay[] = []; // List of course sections.
|
||||||
@Input() initialSectionId?: number; // The section to load first (by ID).
|
@Input() initialSectionId?: number; // The section to load first (by ID).
|
||||||
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
@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() 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.
|
@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.
|
// Always load "All sections" to display the section title. If it isn't there just load the section.
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.sectionChanged(sections[0]);
|
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.
|
// We have an input indicating the section ID to load. Search the section.
|
||||||
const section = sections.find((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.
|
// Don't load the section if it cannot be viewed by the user.
|
||||||
if (section && this.canViewSection(section)) {
|
if (section && this.canViewSection(section)) {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.sectionChanged(section);
|
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) {
|
if (!this.loaded) {
|
||||||
|
@ -666,8 +678,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
CoreCourse.logView(this.course.id, sectionNumber),
|
CoreCourse.logView(this.course.id, sectionNumber),
|
||||||
);
|
);
|
||||||
|
|
||||||
let extraParams = sectionNumber ? `§ion=${sectionNumber}` : '';
|
let extraParams = sectionNumber !== undefined ? `§ion=${sectionNumber}` : '';
|
||||||
if (firstLoad && 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.
|
// 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 &&
|
const courseDisplay = 'courseformatoptions' in this.course &&
|
||||||
this.course.courseformatoptions?.find(option => option.name === 'coursedisplay');
|
this.course.courseformatoptions?.find(option => option.name === 'coursedisplay');
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
|
|
||||||
<core-loading [hideUntil]="dataLoaded && !updatingData">
|
<core-loading [hideUntil]="dataLoaded && !updatingData">
|
||||||
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber"
|
<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-course-format>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -58,6 +58,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
|
||||||
sections?: CoreCourseSection[];
|
sections?: CoreCourseSection[];
|
||||||
sectionId?: number;
|
sectionId?: number;
|
||||||
sectionNumber?: number;
|
sectionNumber?: number;
|
||||||
|
blockInstanceId?: number;
|
||||||
dataLoaded = false;
|
dataLoaded = false;
|
||||||
updatingData = false;
|
updatingData = false;
|
||||||
downloadCourseEnabled = false;
|
downloadCourseEnabled = false;
|
||||||
|
@ -92,6 +93,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon
|
||||||
|
|
||||||
this.sectionId = CoreNavigator.getRouteNumberParam('sectionId');
|
this.sectionId = CoreNavigator.getRouteNumberParam('sectionId');
|
||||||
this.sectionNumber = CoreNavigator.getRouteNumberParam('sectionNumber');
|
this.sectionNumber = CoreNavigator.getRouteNumberParam('sectionNumber');
|
||||||
|
this.blockInstanceId = CoreNavigator.getRouteNumberParam('blockInstanceId');
|
||||||
this.moduleId = CoreNavigator.getRouteNumberParam('moduleId');
|
this.moduleId = CoreNavigator.getRouteNumberParam('moduleId');
|
||||||
this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest');
|
this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest');
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
if (data.sectionId) {
|
if (data.sectionId) {
|
||||||
this.contentsTab.pageParams.sectionId = data.sectionId;
|
this.contentsTab.pageParams.sectionId = data.sectionId;
|
||||||
}
|
}
|
||||||
if (data.sectionNumber) {
|
if (data.sectionNumber !== undefined) {
|
||||||
this.contentsTab.pageParams.sectionNumber = data.sectionNumber;
|
this.contentsTab.pageParams.sectionNumber = data.sectionNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,12 +162,13 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
course: this.course,
|
course: this.course,
|
||||||
sectionId: CoreNavigator.getRouteNumberParam('sectionId'),
|
sectionId: CoreNavigator.getRouteNumberParam('sectionId'),
|
||||||
sectionNumber: CoreNavigator.getRouteNumberParam('sectionNumber'),
|
sectionNumber: CoreNavigator.getRouteNumberParam('sectionNumber'),
|
||||||
|
blockInstanceId: CoreNavigator.getRouteNumberParam('blockInstanceId'),
|
||||||
isGuest: this.isGuest,
|
isGuest: this.isGuest,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.module) {
|
if (this.module) {
|
||||||
this.contentsTab.pageParams.moduleId = this.module.id;
|
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.
|
// No section specified, use module section.
|
||||||
this.contentsTab.pageParams.sectionId = this.module.section;
|
this.contentsTab.pageParams.sectionId = this.module.section;
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,12 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler
|
||||||
|
|
||||||
if (!isNaN(sectionNumber)) {
|
if (!isNaN(sectionNumber)) {
|
||||||
pageParams.sectionNumber = sectionNumber;
|
pageParams.sectionNumber = sectionNumber;
|
||||||
|
} else {
|
||||||
|
const matches = url.match(/#inst(\d+)/);
|
||||||
|
|
||||||
|
if (matches && matches[1]) {
|
||||||
|
pageParams.blockInstanceId = parseInt(matches[1], 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
|
@ -136,7 +142,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler
|
||||||
// Direct access.
|
// Direct access.
|
||||||
const course = await CoreUtils.ignoreErrors(CoreCourses.getUserCourse(courseId), { id: courseId });
|
const course = await CoreUtils.ignoreErrors(CoreCourses.getUserCourse(courseId), { id: courseId });
|
||||||
|
|
||||||
CoreCourseHelper.openCourse(course, pageParams);
|
CoreCourseHelper.openCourse(course, { params: pageParams });
|
||||||
} else {
|
} else {
|
||||||
this.navigateCourseSummary(courseId, pageParams);
|
this.navigateCourseSummary(courseId, pageParams);
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreCustomURLSchemes } from '@services/urlschemes';
|
import { CoreCustomURLSchemes } from '@services/urlschemes';
|
||||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
|
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
|
||||||
|
@ -161,13 +160,10 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy {
|
||||||
CoreCustomURLSchemes.treatHandleCustomURLError(error);
|
CoreCustomURLSchemes.treatHandleCustomURLError(error);
|
||||||
});
|
});
|
||||||
} else if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { // Check if it's a URL.
|
} else if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { // Check if it's a URL.
|
||||||
// Check if the app can handle the URL.
|
await CoreSites.visitLink(text, {
|
||||||
const treated = await CoreContentLinksHelper.handleLink(text, undefined, true, true);
|
checkRoot: true,
|
||||||
|
openBrowserRoot: true,
|
||||||
if (!treated) {
|
});
|
||||||
// Can't handle it, open it in browser.
|
|
||||||
CoreSites.getCurrentSite()?.openInBrowserWithAutoLogin(text);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// It's not a URL, open it in a modal so the user can see it and copy it.
|
// 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, {
|
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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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 { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||||
|
|
||||||
import { CoreSearchComponentsModule } from './components/components.module';
|
import { CoreSearchComponentsModule } from './components/components.module';
|
||||||
import { SITE_SCHEMA } from './services/search-history-db';
|
import { SITE_SCHEMA } from './services/search-history-db';
|
||||||
import { CoreSearchHistoryProvider } from './services/search-history.service';
|
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>[] = [
|
export const CORE_SEARCH_SERVICES: Type<unknown>[] = [
|
||||||
CoreSearchHistoryProvider,
|
CoreSearchHistoryProvider,
|
||||||
|
CoreSearchGlobalSearchService,
|
||||||
|
];
|
||||||
|
|
||||||
|
const mainMenuChildrenRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: CORE_SEARCH_PAGE_NAME,
|
||||||
|
loadChildren: () => import('./search-lazy.module').then(m => m.CoreSearchLazyModule),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CoreSearchComponentsModule,
|
CoreSearchComponentsModule,
|
||||||
|
CoreMainMenuTabRoutingModule.forChild(mainMenuChildrenRoutes),
|
||||||
|
CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: CORE_SITE_SCHEMAS, useValue: [SITE_SCHEMA], multi: true },
|
{ 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 {}
|
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 { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { Params } from '@angular/router';
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
|
||||||
import { CoreSite, CoreSiteConfig } from '@classes/site';
|
import { CoreSite, CoreSiteConfig } from '@classes/site';
|
||||||
import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course';
|
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 { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreTime } from '@singletons/time';
|
import { CoreTime } from '@singletons/time';
|
||||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||||
|
import { CoreBlockSideBlocksComponent } from '@features/block/components/side-blocks/side-blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays site home index.
|
* Page that displays site home index.
|
||||||
|
@ -58,7 +59,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
||||||
protected updateSiteObserver: CoreEventObserver;
|
protected updateSiteObserver: CoreEventObserver;
|
||||||
protected logView: () => void;
|
protected logView: () => void;
|
||||||
|
|
||||||
constructor() {
|
constructor(protected route: ActivatedRoute) {
|
||||||
// Refresh the enabled flags if site is updated.
|
// Refresh the enabled flags if site is updated.
|
||||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||||
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
||||||
|
@ -102,6 +103,10 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
||||||
this.loadContent().finally(() => {
|
this.loadContent().finally(() => {
|
||||||
this.dataLoaded = true;
|
this.dataLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.openFocusedInstance();
|
||||||
|
|
||||||
|
this.route.queryParams.subscribe(() => this.openFocusedInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,4 +231,22 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
||||||
this.updateSiteObserver.off();
|
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 { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSiteHomeHomeHandlerService } from './sitehome-home';
|
import { CoreSiteHomeHomeHandlerService } from './sitehome-home';
|
||||||
import { CoreMainMenuHomeHandlerService } from '@features/mainmenu/services/handlers/mainmenu';
|
import { CoreMainMenuHomeHandlerService } from '@features/mainmenu/services/handlers/mainmenu';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to treat links to site home index.
|
* Handler to treat links to site home index.
|
||||||
|
@ -36,7 +37,14 @@ export class CoreSiteHomeIndexLinkHandlerService extends CoreContentLinksHandler
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @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 [{
|
return [{
|
||||||
action: (siteId: string): void => {
|
action: (siteId: string): void => {
|
||||||
CoreNavigator.navigateToSitePath(
|
CoreNavigator.navigateToSitePath(
|
||||||
|
@ -44,6 +52,7 @@ export class CoreSiteHomeIndexLinkHandlerService extends CoreContentLinksHandler
|
||||||
{
|
{
|
||||||
preferCurrentTab: false,
|
preferCurrentTab: false,
|
||||||
siteId,
|
siteId,
|
||||||
|
params: pageParams,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,6 +45,7 @@ describe('Site Home link handlers', () => {
|
||||||
expect(CoreNavigator.navigateToSitePath).toHaveBeenCalledWith('/home/site', {
|
expect(CoreNavigator.navigateToSitePath).toHaveBeenCalledWith('/home/site', {
|
||||||
siteId,
|
siteId,
|
||||||
preferCurrentTab: false,
|
preferCurrentTab: false,
|
||||||
|
params: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ import { CoreNetwork } from '@services/network';
|
||||||
import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config';
|
import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config';
|
||||||
import { CoreLang, CoreLangFormat } from '@services/lang';
|
import { CoreLang, CoreLangFormat } from '@services/lang';
|
||||||
import { CoreNative } from '@features/native/services/native';
|
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_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
|
||||||
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id';
|
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id';
|
||||||
|
@ -707,6 +708,39 @@ export class CoreSitesProvider {
|
||||||
return CoreConstants.CONFIG.wsservice;
|
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.
|
* Check for the minimum required version.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1506,6 +1506,28 @@ export class CoreDomUtilsProvider {
|
||||||
return loading;
|
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.
|
* 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 englishTranslations from '@/assets/lang/en.json';
|
||||||
import { CoreApplicationInitStatus } from '@classes/application-init-status';
|
import { CoreApplicationInitStatus } from '@classes/application-init-status';
|
||||||
import { Translate } from '@singletons';
|
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.
|
// For translate loader. AoT requires an exported function for factories.
|
||||||
export class StaticTranslateLoader extends TranslateLoader {
|
export class StaticTranslateLoader extends TranslateLoader {
|
||||||
|
@ -45,12 +53,17 @@ export class StaticTranslateLoader extends TranslateLoader {
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus },
|
{ provide: ApplicationInitStatus, useClass: CoreApplicationInitStatus },
|
||||||
|
{ provide: CoreSitesProvider, useClass: CoreSitesProviderStub },
|
||||||
|
{ provide: CoreDbProvider, useClass: CoreDbProviderStub },
|
||||||
|
{ provide: CoreFilepoolProvider, useClass: CoreFilepoolProviderStub },
|
||||||
|
{ provide: HttpClient, useClass: HttpClientStub },
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
multi: true,
|
multi: true,
|
||||||
useValue: () => {
|
useValue: () => {
|
||||||
Translate.setDefaultLang('en');
|
Translate.setDefaultLang('en');
|
||||||
Translate.use('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