Skip to content

Commit 4b993e4

Browse files
authored
Merge pull request #13008 from AlexVelezLl/integrate-search-resource-selection
Integrate search resource selection
2 parents 0a64cbc + 696cfa9 commit 4b993e4

File tree

22 files changed

+799
-105
lines changed

22 files changed

+799
-105
lines changed

kolibri/plugins/coach/assets/src/app.js

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ class CoachToolsModule extends KolibriApp {
7272
PageNames.LESSON_PREVIEW_SELECTED_RESOURCES,
7373
PageNames.LESSON_PREVIEW_RESOURCE,
7474
PageNames.LESSON_SELECT_RESOURCES_INDEX,
75+
PageNames.LESSON_SELECT_RESOURCES_SEARCH,
76+
PageNames.LESSON_SELECT_RESOURCES_SEARCH_RESULTS,
7577
PageNames.LESSON_SELECT_RESOURCES_BOOKMARKS,
7678
PageNames.LESSON_SELECT_RESOURCES_TOPIC_TREE,
7779
];

kolibri/plugins/coach/assets/src/composables/useResourceSelection.js

+68-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import uniqBy from 'lodash/uniqBy';
22
import { ref, computed, getCurrentInstance, watch } from 'vue';
33
import ContentNodeResource from 'kolibri-common/apiResources/ContentNodeResource';
44
import ChannelResource from 'kolibri-common/apiResources/ChannelResource';
5+
import useBaseSearch from 'kolibri-common/composables/useBaseSearch';
56
import useFetch from './useFetch';
67

78
/**
@@ -13,6 +14,11 @@ import useFetch from './useFetch';
1314
* This utility handles selection rules, manages fetch states for channels, bookmarks,
1415
* and topic trees, and offers methods to add, remove, or override selected resources.
1516
*
17+
* @param {Object} options
18+
* @param {string} options.searchResultsRouteName The name of the route where the search results
19+
* will be displayed so that we can redirect to it when the search terms are updated.
20+
*
21+
*
1622
* @typedef {Object} UseResourceSelectionResponse
1723
* @property {Object} topic Topic tree object, contains the information of the topic,
1824
* its ascendants and children.
@@ -26,6 +32,10 @@ import useFetch from './useFetch';
2632
* fetching bookmarks. Fetching more bookmarks is supported.
2733
* @property {FetchObject} treeFetch Topic tree fetch object to manage the process of
2834
* fetching topic trees and their resources. Fetching more resources is supported.
35+
* @property {FetchObject} searchFetch Search fetch object to manage the process of
36+
* fetching search results. Fetching more search results is supported.
37+
* @property {Array<string>} searchTerms The search terms used to filter the search results.
38+
* @property {boolean} displayingSearchResults Indicates whether we currently have search terms.
2939
* @property {Array<(node: Object) => boolean>} selectionRules An array of functions that determine
3040
* whether a node can be selected.
3141
* @property {Array<Object>} selectedResources An array of currently selected resources.
@@ -35,10 +45,13 @@ import useFetch from './useFetch';
3545
* from the `selectedResources` array.
3646
* @property {(resources: Array<Object>) => void} setSelectedResources Replaces the current
3747
* `selectedResources` array with the provided resources array.
48+
* @property {() => void} clearSearch Clears the current search terms and results.
49+
* @property {(tag: Object) => void} removeSearchFilterTag Removes the specified tag from the
50+
* search terms.
3851
*
3952
* @returns {UseResourceSelectionResponse}
4053
*/
41-
export default function useResourceSelection() {
54+
export default function useResourceSelection({ searchResultsRouteName } = {}) {
4255
const store = getCurrentInstance().proxy.$store;
4356
const route = computed(() => store.state.route);
4457
const topicId = computed(() => route.value.query.topicId);
@@ -67,8 +80,49 @@ export default function useResourceSelection() {
6780
}),
6881
});
6982

83+
const waitForTopicLoad = () => {
84+
const { searchTopicId } = route.value.query;
85+
const topicToWaitFor = searchTopicId || topicId.value;
86+
if (!topicToWaitFor || topicToWaitFor === topic.value?.id) {
87+
return Promise.resolve();
88+
}
89+
return new Promise(resolve => {
90+
const unwatch = watch(topic, () => {
91+
if (topic.value?.id === topicToWaitFor) {
92+
unwatch();
93+
resolve();
94+
}
95+
});
96+
});
97+
};
98+
99+
const useSearchObject = useBaseSearch({
100+
descendant: topic,
101+
searchResultsRouteName,
102+
// As we dont always show the search filters, we dont need to reload the search results
103+
// each time the topic changes if not needed
104+
reloadOnDescendantChange: false,
105+
});
106+
const searchFetch = {
107+
data: useSearchObject.results,
108+
loading: useSearchObject.searchLoading,
109+
hasMore: computed(() => !!useSearchObject.more.value),
110+
loadingMore: useSearchObject.moreLoading,
111+
fetchData: async () => {
112+
// Make sure that the topic is loaded before searching
113+
await waitForTopicLoad();
114+
return useSearchObject.search();
115+
},
116+
fetchMore: useSearchObject.searchMore,
117+
};
118+
119+
const { displayingSearchResults } = useSearchObject;
120+
70121
const fetchTree = async (params = {}) => {
71-
topic.value = await ContentNodeResource.fetchTree(params);
122+
const newTopic = await ContentNodeResource.fetchTree(params);
123+
if (topic.value?.id !== newTopic.id) {
124+
topic.value = newTopic;
125+
}
72126
return topic.value.children;
73127
};
74128

@@ -80,11 +134,13 @@ export default function useResourceSelection() {
80134
watch(topicId, () => {
81135
if (topicId.value) {
82136
treeFetch.fetchData();
137+
} else {
138+
topic.value = null;
83139
}
84140
});
85141

86142
const loading = computed(() => {
87-
const sources = [bookmarksFetch, channelsFetch, treeFetch];
143+
const sources = [bookmarksFetch, channelsFetch, treeFetch, searchFetch];
88144

89145
return sources.some(sourceFetch => sourceFetch.loading.value);
90146
});
@@ -95,6 +151,9 @@ export default function useResourceSelection() {
95151
if (topicId.value) {
96152
treeFetch.fetchData();
97153
}
154+
if (displayingSearchResults.value) {
155+
searchFetch.fetchData();
156+
}
98157
};
99158

100159
fetchInitialData();
@@ -129,13 +188,18 @@ export default function useResourceSelection() {
129188
return {
130189
topic,
131190
loading,
191+
treeFetch,
132192
channelsFetch,
133193
bookmarksFetch,
134-
treeFetch,
194+
searchFetch,
135195
selectionRules,
136196
selectedResources,
197+
searchTerms: useSearchObject.searchTerms,
198+
displayingSearchResults: useSearchObject.displayingSearchResults,
137199
selectResources,
138200
deselectResources,
139201
setSelectedResources,
202+
clearSearch: useSearchObject.clearSearch,
203+
removeSearchFilterTag: useSearchObject.removeFilterTag,
140204
};
141205
}

kolibri/plugins/coach/assets/src/constants/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ export const PageNames = {
3636
LESSON_EDIT_DETAILS_BETTER: 'LESSON_EDIT_DETAILS_BETTER',
3737
LESSON_SELECT_RESOURCES: 'LESSON_SELECT_RESOURCES',
3838
LESSON_SELECT_RESOURCES_INDEX: 'LESSON_SELECT_RESOURCES_INDEX',
39+
LESSON_SELECT_RESOURCES_SEARCH: 'LESSON_SELECT_RESOURCES_SEARCH',
3940
LESSON_SELECT_RESOURCES_BOOKMARKS: 'LESSON_SELECT_RESOURCES_BOOKMARKS',
4041
LESSON_SELECT_RESOURCES_TOPIC_TREE: 'LESSON_SELECT_RESOURCES_TOPIC_TREE',
42+
LESSON_SELECT_RESOURCES_SEARCH_RESULTS: 'LESSON_SELECT_RESOURCES_SEARCH_RESULTS',
4143
LESSON_PREVIEW_SELECTED_RESOURCES: 'LESSON_PREVIEW_SELECTED_RESOURCES',
4244
LESSON_PREVIEW_RESOURCE: 'LESSON_PREVIEW_RESOURCE',
4345
LESSON_LEARNER_REPORT: 'LESSON_LEARNER_REPORT',

kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ import LessonLearnerExercisePage from '../views/lessons/reports/LessonLearnerExe
3838
import QuestionLearnersPage from '../views/common/reports/QuestionLearnersPage.vue';
3939
import EditLessonDetails from '../views/lessons/LessonSummaryPage/sidePanels/EditLessonDetails';
4040
import PreviewSelectedResources from '../views/lessons/LessonSummaryPage/sidePanels/PreviewSelectedResources';
41-
import LessonResourceSelection from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection';
41+
import LessonResourceSelection from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue';
42+
import SearchFilters from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SearchFilters.vue';
4243
import SelectionIndex from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectionIndex.vue';
4344
import SelectFromBookmarks from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromBookmarks.vue';
44-
import SelectFromChannels from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromChannels.vue';
45+
import SelectFromTopicTree from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromTopicTree.vue';
46+
import SelectFromSearchResults from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue';
4547
import ManageSelectedResources from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedResources.vue';
46-
4748
import { classIdParamRequiredGuard, RouteSegments } from './utils';
4849

4950
const {
@@ -153,8 +154,18 @@ export default [
153154
},
154155
{
155156
name: PageNames.LESSON_SELECT_RESOURCES_TOPIC_TREE,
156-
path: 'channels',
157-
component: SelectFromChannels,
157+
path: 'topic-tree',
158+
component: SelectFromTopicTree,
159+
},
160+
{
161+
name: PageNames.LESSON_SELECT_RESOURCES_SEARCH,
162+
path: 'search',
163+
component: SearchFilters,
164+
},
165+
{
166+
name: PageNames.LESSON_SELECT_RESOURCES_SEARCH_RESULTS,
167+
path: 'search-results',
168+
component: SelectFromSearchResults,
158169
},
159170
{
160171
name: PageNames.LESSON_PREVIEW_SELECTED_RESOURCES,

kolibri/plugins/coach/assets/src/views/lessons/LessonResourceSelectionPage/ContentCardList.vue

+6
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
:content="content"
4949
:message="contentCardMessage(content)"
5050
:link="contentCardLink(content)"
51+
:headingLevel="cardsHeadingLevel"
5152
>
5253
<template #notice>
5354
<slot
@@ -161,6 +162,11 @@
161162
type: Function, // ContentNode => Route
162163
required: true,
163164
},
165+
// Heading level for the cards
166+
cardsHeadingLevel: {
167+
type: Number,
168+
default: 3,
169+
},
164170
},
165171
166172
computed: {

kolibri/plugins/coach/assets/src/views/lessons/LessonResourceSelectionPage/LessonContentCard/index.vue

+20-2
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@
1313
:class="{ 'title-message-wrapper': Boolean(!windowIsSmall) }"
1414
:style="{ color: $themeTokens.text }"
1515
>
16-
<h3
16+
<component
17+
:is="headingElement"
1718
class="title"
1819
dir="auto"
1920
>
2021
<KTextTruncator
2122
:text="content.title"
2223
:maxLines="2"
2324
/>
24-
</h3>
25+
</component>
2526
</div>
2627
<KTextTruncator
2728
v-if="!windowIsSmall"
@@ -101,11 +102,27 @@
101102
type: String,
102103
default: '',
103104
},
105+
headingLevel: {
106+
type: Number,
107+
default: 3,
108+
validator(value) {
109+
if (value <= 6 && value >= 2) {
110+
return true;
111+
} else {
112+
// eslint-disable-next-line no-console
113+
console.error(`'headingLevel' must be between 2 and 6.`);
114+
return false;
115+
}
116+
},
117+
},
104118
},
105119
computed: {
106120
isTopic() {
107121
return !this.content.isLeaf;
108122
},
123+
headingElement() {
124+
return `h${this.headingLevel}`;
125+
},
109126
},
110127
};
111128
@@ -151,6 +168,7 @@
151168
152169
.title {
153170
margin-bottom: 0.5em;
171+
font-size: 1em;
154172
}
155173
156174
.message {

kolibri/plugins/coach/assets/src/views/lessons/LessonResourceSelectionPage/SearchTools/ResourceSelectionBreadcrumbs.vue

+12-10
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,18 @@
2828
},
2929
computed: {
3030
selectionCrumbs() {
31-
return [
32-
// The "Channels" breadcrumb
33-
{ text: this.coreString('channelsLabel'), link: this.channelsLink },
34-
// Ancestors breadcrumbs
35-
// NOTE: The current topic is injected into `ancestors` in the showPage action
36-
...this.ancestors.map(a => ({
37-
text: a.title,
38-
link: this.topicsLink(a.id),
39-
})),
40-
];
31+
// NOTE: The current topic is injected into `ancestors` in the parent component
32+
const breadcrumbs = this.ancestors.map(a => ({
33+
text: a.title,
34+
link: this.topicsLink(a.id),
35+
}));
36+
if (this.channelsLink) {
37+
breadcrumbs.unshift({
38+
text: this.coreString('channelsLabel'),
39+
link: this.channelsLink,
40+
});
41+
}
42+
return breadcrumbs;
4143
},
4244
},
4345
};

kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/UpdatedResourceSelection.vue

+26-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class="select-resource">
44
<div>
55
<ResourceSelectionBreadcrumbs
6-
v-if="topic"
6+
v-if="topic && !hideBreadcrumbs"
77
:ancestors="[...topic.ancestors, topic]"
88
:channelsLink="channelsLink"
99
:topicsLink="topicsLink"
@@ -20,6 +20,7 @@
2020
:contentCheckboxDisabled="contentCheckboxDisabled"
2121
:contentCardLink="contentLink"
2222
:showRadioButtons="!multi"
23+
:cardsHeadingLevel="cardsHeadingLevel"
2324
@changeselectall="handleSelectAll"
2425
@change_content_card="toggleSelected"
2526
@moreresults="fetchMore"
@@ -36,7 +37,7 @@
3637
import { ContentNodeKinds } from 'kolibri/constants';
3738
import ContentCardList from '../../lessons/LessonResourceSelectionPage/ContentCardList.vue';
3839
import ResourceSelectionBreadcrumbs from '../../lessons/LessonResourceSelectionPage/SearchTools/ResourceSelectionBreadcrumbs.vue';
39-
import { PageNames, ViewMoreButtonStates } from '../../../constants';
40+
import { ViewMoreButtonStates } from '../../../constants';
4041
4142
export default {
4243
name: 'UpdatedResourceSelection',
@@ -94,13 +95,26 @@
9495
type: Boolean,
9596
default: false,
9697
},
98+
cardsHeadingLevel: {
99+
type: Number,
100+
default: 3,
101+
},
102+
channelsLink: {
103+
type: Object,
104+
required: false,
105+
default: null,
106+
},
107+
getTopicLink: {
108+
type: Function,
109+
required: false,
110+
default: () => {},
111+
},
112+
hideBreadcrumbs: {
113+
type: Boolean,
114+
default: false,
115+
},
97116
},
98117
computed: {
99-
channelsLink() {
100-
return {
101-
name: PageNames.LESSON_SELECT_RESOURCES_INDEX,
102-
};
103-
},
104118
selectAllIndeterminate() {
105119
return (
106120
!this.selectAllChecked &&
@@ -142,6 +156,11 @@
142156
return { name, params, query };
143157
},
144158
topicsLink(topicId) {
159+
const route = this.getTopicLink?.(topicId);
160+
if (route) {
161+
return route;
162+
}
163+
145164
const { name, params, query } = this.$route;
146165
return {
147166
name,

0 commit comments

Comments
 (0)