Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate search resource selection #13008

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kolibri/plugins/coach/assets/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class CoachToolsModule extends KolibriApp {
PageNames.LESSON_PREVIEW_SELECTED_RESOURCES,
PageNames.LESSON_PREVIEW_RESOURCE,
PageNames.LESSON_SELECT_RESOURCES_INDEX,
PageNames.LESSON_SELECT_RESOURCES_SEARCH,
PageNames.LESSON_SELECT_RESOURCES_SEARCH_RESULTS,
PageNames.LESSON_SELECT_RESOURCES_BOOKMARKS,
PageNames.LESSON_SELECT_RESOURCES_TOPIC_TREE,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import uniqBy from 'lodash/uniqBy';
import { ref, computed, getCurrentInstance, watch } from 'vue';
import ContentNodeResource from 'kolibri-common/apiResources/ContentNodeResource';
import ChannelResource from 'kolibri-common/apiResources/ChannelResource';
import useBaseSearch from 'kolibri-common/composables/useBaseSearch';
import useFetch from './useFetch';

/**
Expand All @@ -13,6 +14,11 @@ import useFetch from './useFetch';
* This utility handles selection rules, manages fetch states for channels, bookmarks,
* and topic trees, and offers methods to add, remove, or override selected resources.
*
* @param {Object} options
* @param {string} options.searchResultsRouteName The name of the route where the search results
* will be displayed so that we can redirect to it when the search terms are updated.
*
*
* @typedef {Object} UseResourceSelectionResponse
* @property {Object} topic Topic tree object, contains the information of the topic,
* its ascendants and children.
Expand All @@ -26,6 +32,10 @@ import useFetch from './useFetch';
* fetching bookmarks. Fetching more bookmarks is supported.
* @property {FetchObject} treeFetch Topic tree fetch object to manage the process of
* fetching topic trees and their resources. Fetching more resources is supported.
* @property {FetchObject} searchFetch Search fetch object to manage the process of
* fetching search results. Fetching more search results is supported.
* @property {Array<string>} searchTerms The search terms used to filter the search results.
* @property {boolean} displayingSearchResults Indicates whether we currently have search terms.
* @property {Array<(node: Object) => boolean>} selectionRules An array of functions that determine
* whether a node can be selected.
* @property {Array<Object>} selectedResources An array of currently selected resources.
Expand All @@ -35,10 +45,13 @@ import useFetch from './useFetch';
* from the `selectedResources` array.
* @property {(resources: Array<Object>) => void} setSelectedResources Replaces the current
* `selectedResources` array with the provided resources array.
* @property {() => void} clearSearch Clears the current search terms and results.
* @property {(tag: Object) => void} removeSearchFilterTag Removes the specified tag from the
* search terms.
*
* @returns {UseResourceSelectionResponse}
*/
export default function useResourceSelection() {
export default function useResourceSelection({ searchResultsRouteName } = {}) {
const store = getCurrentInstance().proxy.$store;
const route = computed(() => store.state.route);
const topicId = computed(() => route.value.query.topicId);
Expand Down Expand Up @@ -67,8 +80,49 @@ export default function useResourceSelection() {
}),
});

const waitForTopicLoad = () => {
const { searchTopicId } = route.value.query;
const topicToWaitFor = searchTopicId || topicId.value;
if (!topicToWaitFor || topicToWaitFor === topic.value?.id) {
return Promise.resolve();
}
return new Promise(resolve => {
const unwatch = watch(topic, () => {
if (topic.value?.id === topicToWaitFor) {
unwatch();
resolve();
}
});
});
};

const useSearchObject = useBaseSearch({
descendant: topic,
searchResultsRouteName,
// As we dont always show the search filters, we dont need to reload the search results
// each time the topic changes if not needed
reloadOnDescendantChange: false,
});
const searchFetch = {
data: useSearchObject.results,
loading: useSearchObject.searchLoading,
hasMore: computed(() => !!useSearchObject.more.value),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be reduced just to hasMore: computed(() => useSearchObject.more.value) ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ozer550! In this case its not the same, because hasMore should be a boolean value, and useSearchObject.more.value is an object, the "more" object. So to transform it to boolean we can do either !!useSearchObject.more.value or Boolean(useSearchObject.more.value), but personally I like more the former 😅.

loadingMore: useSearchObject.moreLoading,
fetchData: async () => {
// Make sure that the topic is loaded before searching
await waitForTopicLoad();
return useSearchObject.search();
},
fetchMore: useSearchObject.searchMore,
};

const { displayingSearchResults } = useSearchObject;

const fetchTree = async (params = {}) => {
topic.value = await ContentNodeResource.fetchTree(params);
const newTopic = await ContentNodeResource.fetchTree(params);
if (topic.value?.id !== newTopic.id) {
topic.value = newTopic;
}
return topic.value.children;
};

Expand All @@ -80,11 +134,13 @@ export default function useResourceSelection() {
watch(topicId, () => {
if (topicId.value) {
treeFetch.fetchData();
} else {
topic.value = null;
}
});

const loading = computed(() => {
const sources = [bookmarksFetch, channelsFetch, treeFetch];
const sources = [bookmarksFetch, channelsFetch, treeFetch, searchFetch];

return sources.some(sourceFetch => sourceFetch.loading.value);
});
Expand All @@ -95,6 +151,9 @@ export default function useResourceSelection() {
if (topicId.value) {
treeFetch.fetchData();
}
if (displayingSearchResults.value) {
searchFetch.fetchData();
}
};

fetchInitialData();
Expand Down Expand Up @@ -129,13 +188,18 @@ export default function useResourceSelection() {
return {
topic,
loading,
treeFetch,
channelsFetch,
bookmarksFetch,
treeFetch,
searchFetch,
selectionRules,
selectedResources,
searchTerms: useSearchObject.searchTerms,
displayingSearchResults: useSearchObject.displayingSearchResults,
selectResources,
deselectResources,
setSelectedResources,
clearSearch: useSearchObject.clearSearch,
removeSearchFilterTag: useSearchObject.removeFilterTag,
};
}
2 changes: 2 additions & 0 deletions kolibri/plugins/coach/assets/src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ export const PageNames = {
LESSON_EDIT_DETAILS_BETTER: 'LESSON_EDIT_DETAILS_BETTER',
LESSON_SELECT_RESOURCES: 'LESSON_SELECT_RESOURCES',
LESSON_SELECT_RESOURCES_INDEX: 'LESSON_SELECT_RESOURCES_INDEX',
LESSON_SELECT_RESOURCES_SEARCH: 'LESSON_SELECT_RESOURCES_SEARCH',
LESSON_SELECT_RESOURCES_BOOKMARKS: 'LESSON_SELECT_RESOURCES_BOOKMARKS',
LESSON_SELECT_RESOURCES_TOPIC_TREE: 'LESSON_SELECT_RESOURCES_TOPIC_TREE',
LESSON_SELECT_RESOURCES_SEARCH_RESULTS: 'LESSON_SELECT_RESOURCES_SEARCH_RESULTS',
LESSON_PREVIEW_SELECTED_RESOURCES: 'LESSON_PREVIEW_SELECTED_RESOURCES',
LESSON_PREVIEW_RESOURCE: 'LESSON_PREVIEW_RESOURCE',
LESSON_LEARNER_REPORT: 'LESSON_LEARNER_REPORT',
Expand Down
21 changes: 16 additions & 5 deletions kolibri/plugins/coach/assets/src/routes/lessonsRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ import LessonLearnerExercisePage from '../views/lessons/reports/LessonLearnerExe
import QuestionLearnersPage from '../views/common/reports/QuestionLearnersPage.vue';
import EditLessonDetails from '../views/lessons/LessonSummaryPage/sidePanels/EditLessonDetails';
import PreviewSelectedResources from '../views/lessons/LessonSummaryPage/sidePanels/PreviewSelectedResources';
import LessonResourceSelection from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection';
import LessonResourceSelection from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue';
import SearchFilters from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SearchFilters.vue';
import SelectionIndex from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectionIndex.vue';
import SelectFromBookmarks from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromBookmarks.vue';
import SelectFromChannels from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromChannels.vue';
import SelectFromTopicTree from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromTopicTree.vue';
import SelectFromSearchResults from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue';
import ManageSelectedResources from '../views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/ManageSelectedResources.vue';

import { classIdParamRequiredGuard, RouteSegments } from './utils';

const {
Expand Down Expand Up @@ -153,8 +154,18 @@ export default [
},
{
name: PageNames.LESSON_SELECT_RESOURCES_TOPIC_TREE,
path: 'channels',
component: SelectFromChannels,
path: 'topic-tree',
component: SelectFromTopicTree,
},
{
name: PageNames.LESSON_SELECT_RESOURCES_SEARCH,
path: 'search',
component: SearchFilters,
},
{
name: PageNames.LESSON_SELECT_RESOURCES_SEARCH_RESULTS,
path: 'search-results',
component: SelectFromSearchResults,
},
{
name: PageNames.LESSON_PREVIEW_SELECTED_RESOURCES,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
:content="content"
:message="contentCardMessage(content)"
:link="contentCardLink(content)"
:headingLevel="cardsHeadingLevel"
>
<template #notice>
<slot
Expand Down Expand Up @@ -161,6 +162,11 @@
type: Function, // ContentNode => Route
required: true,
},
// Heading level for the cards
cardsHeadingLevel: {
type: Number,
default: 3,
},
},

computed: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
:class="{ 'title-message-wrapper': Boolean(!windowIsSmall) }"
:style="{ color: $themeTokens.text }"
>
<h3
<component
:is="headingElement"
class="title"
dir="auto"
>
<KTextTruncator
:text="content.title"
:maxLines="2"
/>
</h3>
</component>
</div>
<KTextTruncator
v-if="!windowIsSmall"
Expand Down Expand Up @@ -101,11 +102,27 @@
type: String,
default: '',
},
headingLevel: {
type: Number,
default: 3,
validator(value) {
if (value <= 6 && value >= 2) {
return true;
} else {
// eslint-disable-next-line no-console
console.error(`'headingLevel' must be between 2 and 6.`);
return false;
}
},
},
},
computed: {
isTopic() {
return !this.content.isLeaf;
},
headingElement() {
return `h${this.headingLevel}`;
},
},
};

Expand Down Expand Up @@ -151,6 +168,7 @@

.title {
margin-bottom: 0.5em;
font-size: 1em;
}

.message {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@
},
computed: {
selectionCrumbs() {
return [
// The "Channels" breadcrumb
{ text: this.coreString('channelsLabel'), link: this.channelsLink },
// Ancestors breadcrumbs
// NOTE: The current topic is injected into `ancestors` in the showPage action
...this.ancestors.map(a => ({
text: a.title,
link: this.topicsLink(a.id),
})),
];
// NOTE: The current topic is injected into `ancestors` in the parent component
const breadcrumbs = this.ancestors.map(a => ({
text: a.title,
link: this.topicsLink(a.id),
}));
if (this.channelsLink) {
breadcrumbs.unshift({
text: this.coreString('channelsLabel'),
link: this.channelsLink,
});
}
return breadcrumbs;
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="select-resource">
<div>
<ResourceSelectionBreadcrumbs
v-if="topic"
v-if="topic && !hideBreadcrumbs"
:ancestors="[...topic.ancestors, topic]"
:channelsLink="channelsLink"
:topicsLink="topicsLink"
Expand All @@ -20,6 +20,7 @@
:contentCheckboxDisabled="contentCheckboxDisabled"
:contentCardLink="contentLink"
:showRadioButtons="!multi"
:cardsHeadingLevel="cardsHeadingLevel"
@changeselectall="handleSelectAll"
@change_content_card="toggleSelected"
@moreresults="fetchMore"
Expand All @@ -36,7 +37,7 @@
import { ContentNodeKinds } from 'kolibri/constants';
import ContentCardList from '../../lessons/LessonResourceSelectionPage/ContentCardList.vue';
import ResourceSelectionBreadcrumbs from '../../lessons/LessonResourceSelectionPage/SearchTools/ResourceSelectionBreadcrumbs.vue';
import { PageNames, ViewMoreButtonStates } from '../../../constants';
import { ViewMoreButtonStates } from '../../../constants';

export default {
name: 'UpdatedResourceSelection',
Expand Down Expand Up @@ -94,13 +95,26 @@
type: Boolean,
default: false,
},
cardsHeadingLevel: {
type: Number,
default: 3,
},
channelsLink: {
type: Object,
required: false,
default: null,
},
getTopicLink: {
type: Function,
required: false,
default: () => {},
},
hideBreadcrumbs: {
type: Boolean,
default: false,
},
},
computed: {
channelsLink() {
return {
name: PageNames.LESSON_SELECT_RESOURCES_INDEX,
};
},
selectAllIndeterminate() {
return (
!this.selectAllChecked &&
Expand Down Expand Up @@ -142,6 +156,11 @@
return { name, params, query };
},
topicsLink(topicId) {
const route = this.getTopicLink?.(topicId);
if (route) {
return route;
}

const { name, params, query } = this.$route;
return {
name,
Expand Down
Loading