diff --git a/scripts/langindex.json b/scripts/langindex.json index 2141ce9ff..93fa25455 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -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", diff --git a/src/addons/mod/forum/services/handlers/module.ts b/src/addons/mod/forum/services/handlers/module.ts index e55e46453..7917e8df8 100644 --- a/src/addons/mod/forum/services/handlers/module.ts +++ b/src/addons/mod/forum/services/handlers/module.ts @@ -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 | undefined> { + const { AddonModForumIndexComponent } = await import('../../components/index'); + return AddonModForumIndexComponent; } diff --git a/src/core/features/search/components/components.module.ts b/src/core/features/search/components/components.module.ts index 2c935312a..730dc349b 100644 --- a/src/core/features/search/components/components.module.ts +++ b/src/core/features/search/components/components.module.ts @@ -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 {} diff --git a/src/core/features/search/components/global-search-result/global-search-result.html b/src/core/features/search/components/global-search-result/global-search-result.html new file mode 100644 index 000000000..b5a1b7d4d --- /dev/null +++ b/src/core/features/search/components/global-search-result/global-search-result.html @@ -0,0 +1,23 @@ + + + + +

+ + + +

+ +
+
+ + +
+
+ + {{ 'core.search.resultby' | translate: { $a: result.context.userName } }} +
+
+
+
diff --git a/src/core/features/search/components/global-search-result/global-search-result.scss b/src/core/features/search/components/global-search-result/global-search-result.scss new file mode 100644 index 000000000..d508e5985 --- /dev/null +++ b/src/core/features/search/components/global-search-result/global-search-result.scss @@ -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); +} diff --git a/src/core/features/search/components/global-search-result/global-search-result.ts b/src/core/features/search/components/global-search-result/global-search-result.ts new file mode 100644 index 000000000..8b952b009 --- /dev/null +++ b/src/core/features/search/components/global-search-result/global-search-result.ts @@ -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; + } + +} diff --git a/src/core/features/search/lang.json b/src/core/features/search/lang.json new file mode 100644 index 000000000..f242e9e97 --- /dev/null +++ b/src/core/features/search/lang.json @@ -0,0 +1,3 @@ +{ + "resultby": "By {{$a}}" +} diff --git a/src/core/features/search/services/global-search.ts b/src/core/features/search/services/global-search.ts new file mode 100644 index 000000000..5706c5073 --- /dev/null +++ b/src/core/features/search/services/global-search.ts @@ -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; +}; diff --git a/src/core/features/search/stories/components/components.module.ts b/src/core/features/search/stories/components/components.module.ts new file mode 100644 index 000000000..5a2a88bac --- /dev/null +++ b/src/core/features/search/stories/components/components.module.ts @@ -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 {} diff --git a/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.html b/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.html new file mode 100644 index 000000000..7625c8827 --- /dev/null +++ b/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.html @@ -0,0 +1,18 @@ + + + + +

Search Results

+
+
+
+ +
+ + + + + +
+
+
diff --git a/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.ts b/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.ts new file mode 100644 index 000000000..500de398d --- /dev/null +++ b/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.ts @@ -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}`); + } + +} diff --git a/src/core/features/search/stories/global-search-result.stories.ts b/src/core/features/search/stories/global-search-result.stories.ts new file mode 100644 index 000000000..125f4666b --- /dev/null +++ b/src/core/features/search/stories/global-search-result.stories.ts @@ -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 > { + 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(({ 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(Template); +export const ResultsPage = story(() => ({ component: CoreSearchGlobalSearchResultsPageComponent }));