MOBILE-3371 search: Implement global search result
parent
9e1bcaf581
commit
00f6ec3d46
|
@ -2310,6 +2310,7 @@
|
|||
"core.scanqr": "local_moodlemobileapp",
|
||||
"core.scrollbackward": "local_moodlemobileapp",
|
||||
"core.scrollforward": "local_moodlemobileapp",
|
||||
"core.search.resultby": "local_moodlemobileapp",
|
||||
"core.search": "moodle",
|
||||
"core.searching": "local_moodlemobileapp",
|
||||
"core.searchresults": "moodle",
|
||||
|
|
|
@ -19,7 +19,6 @@ import { CoreEvents } from '@singletons/events';
|
|||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||
import { CoreConstants, ModPurpose } from '@/core/constants';
|
||||
import { AddonModForumIndexComponent } from '../../components/index';
|
||||
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
|
||||
import { CoreCourseModuleData } from '@features/course/services/course-helper';
|
||||
import { CoreIonicColorNames } from '@singletons/colors';
|
||||
|
@ -86,6 +85,8 @@ export class AddonModForumModuleHandlerService extends CoreModuleHandlerBase imp
|
|||
* @inheritdoc
|
||||
*/
|
||||
async getMainComponent(): Promise<Type<unknown> | undefined> {
|
||||
const { AddonModForumIndexComponent } = await import('../../components/index');
|
||||
|
||||
return AddonModForumIndexComponent;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,16 +16,19 @@ import { NgModule } from '@angular/core';
|
|||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreSearchBoxComponent } from './search-box/search-box';
|
||||
import { CoreSearchGlobalSearchResultComponent } from '@features/search/components/global-search-result/global-search-result';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreSearchBoxComponent,
|
||||
CoreSearchGlobalSearchResultComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
exports: [
|
||||
CoreSearchBoxComponent,
|
||||
CoreSearchGlobalSearchResultComponent,
|
||||
],
|
||||
})
|
||||
export class CoreSearchComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<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>
|
||||
<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,86 @@
|
|||
: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);
|
||||
--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: 16px;
|
||||
--filter: var(--mod-icon-filter);
|
||||
|
||||
margin-inline-end: var(--spacing-2);
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
padding: 0px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
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 {
|
||||
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,3 @@
|
|||
{
|
||||
"resultby": "By {{$a}}"
|
||||
}
|
|
@ -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 { CoreCourseListItem } from '@features/courses/services/courses';
|
||||
import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar';
|
||||
|
||||
export type CoreSearchGlobalSearchResult = {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
content?: string;
|
||||
context?: CoreSearchGlobalSearchResultContext;
|
||||
module?: CoreSearchGlobalSearchResultModule;
|
||||
course?: CoreCourseListItem;
|
||||
user?: CoreUserWithAvatar;
|
||||
};
|
||||
|
||||
export type CoreSearchGlobalSearchResultContext = {
|
||||
userName?: string;
|
||||
courseName?: string;
|
||||
};
|
||||
|
||||
export type CoreSearchGlobalSearchResultModule = {
|
||||
name: string;
|
||||
iconurl: string;
|
||||
area: string;
|
||||
};
|
|
@ -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 { 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';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreSearchGlobalSearchResultsPageComponent,
|
||||
],
|
||||
imports: [
|
||||
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,97 @@
|
|||
// (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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 }));
|
Loading…
Reference in New Issue