From 998809a4c77c603f4f6debc051d24a7d7426ee79 Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Fri, 16 Aug 2024 12:56:22 -0500 Subject: [PATCH] Remove useSearch in favor of useBaseSearch --- .../src/composables/__mocks__/useSearch.js | 88 ---- .../composables/__tests__/useSearch.spec.js | 466 ----------------- .../src/composables/useLearnerResources.js | 2 +- .../learn/assets/src/composables/useSearch.js | 482 ------------------ .../assets/src/views/LibraryPage/index.vue | 21 +- .../ActivityButtonsGroup.vue | 5 +- .../CategorySearchOptions.vue | 5 +- .../views/SearchFiltersPanel/SelectGroup.vue | 4 +- .../src/views/SearchFiltersPanel/index.vue | 4 +- .../assets/src/views/TopicsPage/index.vue | 19 +- .../assets/test/views/library-page.spec.js | 18 +- .../test/views/resumable-content-grid.spec.js | 2 +- .../test/views/search-results-grid.spec.js | 6 +- .../assets/test/views/topics-page.spec.js | 22 +- .../composables/__mocks__/useBaseSearch.js | 2 +- .../composables/useBaseSearch.js | 37 +- .../kolibri-common}/utils/contentNode.js | 0 17 files changed, 85 insertions(+), 1098 deletions(-) delete mode 100644 kolibri/plugins/learn/assets/src/composables/__mocks__/useSearch.js delete mode 100644 kolibri/plugins/learn/assets/src/composables/__tests__/useSearch.spec.js delete mode 100644 kolibri/plugins/learn/assets/src/composables/useSearch.js rename {kolibri/plugins/learn/assets/src => packages/kolibri-common}/utils/contentNode.js (100%) diff --git a/kolibri/plugins/learn/assets/src/composables/__mocks__/useSearch.js b/kolibri/plugins/learn/assets/src/composables/__mocks__/useSearch.js deleted file mode 100644 index 82aba1a2218..00000000000 --- a/kolibri/plugins/learn/assets/src/composables/__mocks__/useSearch.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * `useSearch` composable function mock. - * - * If default values are sufficient for tests, - * you only need call `jest.mock('')` - * at the top of a test file. - * - * If you need to override some default values from some tests, - * you can import a helper function `useSearchMock` that accepts - * an object with values to be overriden and use it together - * with `mockImplementation` as follows: - * - * ``` - * // eslint-disable-next-line import/named - * import useSearch, { useSearchMock } from ''; - * - * jest.mock('') - * - * it('test', () => { - * useSearch.mockImplementation( - * () => useSearchMock({ classes: [{ id: 'class-1' }] }) - * ); - * }) - * ``` - * - * You can reset your mock implementation back to default values - * for other tests by calling the following in `beforeEach`: - * - * ``` - * useSearch.mockImplementation(() => useSearchMock()) - * ``` - */ - -const MOCK_DEFAULTS = { - searchTerms: { - learning_activities: {}, - categories: {}, - learner_needs: {}, - channels: {}, - accessibility_labels: {}, - languages: {}, - grade_levels: {}, - }, - displayingSearchResults: false, - searchLoading: false, - moreLoading: false, - results: [], - more: null, - labels: null, - search: jest.fn(), - searchMore: jest.fn(), - removeFilterTag: jest.fn(), - clearSearch: jest.fn(), - currentRoute: jest.fn(() => { - // return a $route-flavored object to avoid undefined errors - return { - params: {}, - query: {}, - path: '', - fullPath: '', - name: '', - meta: {}, - }; - }), -}; - -export function useSearchMock(overrides = {}) { - return { - ...MOCK_DEFAULTS, - ...overrides, - }; -} - -export default jest.fn(() => useSearchMock()); - -export const injectSearch = jest.fn(() => ({ - availableLearningActivities: [], - availableLibraryCategories: [], - availableResourcesNeeded: [], - availableGradeLevels: [], - availableAccessibilityOptions: [], - availableLanguages: [], - availableChannels: [], - searchableLabels: [], - activeSearchTerms: [], -})); - -export const searchKeys = []; diff --git a/kolibri/plugins/learn/assets/src/composables/__tests__/useSearch.spec.js b/kolibri/plugins/learn/assets/src/composables/__tests__/useSearch.spec.js deleted file mode 100644 index 00ecf194610..00000000000 --- a/kolibri/plugins/learn/assets/src/composables/__tests__/useSearch.spec.js +++ /dev/null @@ -1,466 +0,0 @@ -import { get, set } from '@vueuse/core'; -import VueRouter from 'vue-router'; -import Vue from 'vue'; -import { ref } from 'kolibri.lib.vueCompositionApi'; -import { ContentNodeResource } from 'kolibri.resources'; -import { coreStoreFactory } from 'kolibri.coreVue.vuex.store'; -import { AllCategories, NoCategories } from 'kolibri.coreVue.vuex.constants'; -import useSearch from '../useSearch'; - -Vue.use(VueRouter); - -const name = 'not important'; - -function prep(query = {}, descendant = null) { - const store = coreStoreFactory({ - state: () => ({ - route: { - query, - name, - }, - }), - mutations: { - SET_QUERY(state, query) { - state.route.query = query; - }, - }, - }); - const router = new VueRouter(); - router.push = jest.fn().mockReturnValue(Promise.resolve()); - return { - ...useSearch(descendant, store, router), - router, - store, - }; -} - -describe(`useSearch`, () => { - beforeEach(() => { - ContentNodeResource.fetchCollection = jest.fn(); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - }); - describe(`searchTerms computed ref`, () => { - it(`returns an object with all relevant keys when query params are empty`, () => { - const { searchTerms } = prep(); - expect(get(searchTerms)).toEqual({ - accessibility_labels: {}, - categories: {}, - channels: {}, - grade_levels: {}, - languages: {}, - learner_needs: {}, - learning_activities: {}, - keywords: '', - }); - }); - it(`returns an object with all relevant keys when query params have other keys`, () => { - const { searchTerms } = prep({ - search: { - this: true, - }, - keyword: 'how about this?', - }); - expect(get(searchTerms)).toEqual({ - accessibility_labels: {}, - categories: {}, - channels: {}, - grade_levels: {}, - languages: {}, - learner_needs: {}, - learning_activities: {}, - keywords: '', - }); - }); - it(`returns an object with all relevant keys when query params are specified`, () => { - const { searchTerms } = prep({ - accessibility_labels: 'test1,test2', - keywords: 'I love paris in the springtime!', - categories: 'notatest,reallynotatest,absolutelynotatest', - channels: 'channelid1,channelid2,channelid3', - grade_levels: 'lowerprimary,uppersecondary,adult', - languages: 'ar-jk,en-pr,en-gb', - learner_needs: 'internet,pencil,rolodex', - learning_activities: 'watch', - }); - expect(get(searchTerms)).toEqual({ - accessibility_labels: { - test1: true, - test2: true, - }, - categories: { - notatest: true, - reallynotatest: true, - absolutelynotatest: true, - }, - channels: { - channelid1: true, - channelid2: true, - channelid3: true, - }, - grade_levels: { - lowerprimary: true, - uppersecondary: true, - adult: true, - }, - languages: { - 'ar-jk': true, - 'en-pr': true, - 'en-gb': true, - }, - learner_needs: { - internet: true, - pencil: true, - rolodex: true, - }, - learning_activities: { - watch: true, - }, - keywords: 'I love paris in the springtime!', - }); - }); - it(`setting relevant keys will result in a router push`, () => { - const { searchTerms, router } = prep(); - set(searchTerms, { - keywords: 'test', - categories: { - cat1: true, - cat2: true, - }, - }); - expect(router.push).toHaveBeenCalledWith({ - name, - query: { - keywords: 'test', - categories: 'cat1,cat2', - }, - }); - }); - it(`removing keys will be propagated to the router`, () => { - const { searchTerms, router } = prep({ - keywords: 'test', - categories: 'cat1,cat2', - grade_levels: 'level1', - }); - set(searchTerms, { - keywords: '', - categories: { - cat2: true, - }, - }); - expect(router.push).toHaveBeenCalledWith({ - name, - query: { - categories: 'cat2', - }, - }); - }); - it(`setting keywords to null will be propagated to the router`, () => { - const { searchTerms, router } = prep({ - keywords: 'test', - categories: 'cat1,cat2', - grade_levels: 'level1', - }); - set(searchTerms, { - keywords: null, - categories: { - cat2: true, - }, - }); - expect(router.push).toHaveBeenCalledWith({ - name, - query: { - categories: 'cat2', - }, - }); - }); - }); - describe('displayingSearchResults computed property', () => { - const searchKeys = [ - 'learning_activities', - 'categories', - 'learner_needs', - 'channels', - 'accessibility_labels', - 'languages', - 'grade_levels', - ]; - it.each(searchKeys)('should be true when there are any values for %s', key => { - const { displayingSearchResults } = prep({ - [key]: 'test1,test2', - }); - expect(get(displayingSearchResults)).toBe(true); - }); - it('should be true when there is a value for keywords', () => { - const { displayingSearchResults } = prep({ - keywords: 'testing testing one two three', - }); - expect(get(displayingSearchResults)).toBe(true); - }); - }); - describe('search method', () => { - it('should call ContentNodeResource.fetchCollection when searchTerms changes', async () => { - const { store } = prep(); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - store.commit('SET_QUERY', { categories: 'test1,test2' }); - await Vue.nextTick(); - expect(ContentNodeResource.fetchCollection).toHaveBeenCalledWith({ - getParams: { - categories: ['test1', 'test2'], - max_results: 25, - include_coach_content: false, - }, - }); - }); - it('should not call ContentNodeResource.fetchCollection if there is no search', () => { - const { search } = prep(); - ContentNodeResource.fetchCollection.mockClear(); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - search(); - expect(ContentNodeResource.fetchCollection).not.toHaveBeenCalled(); - }); - it('should clear labels and more if there is no search', () => { - const { search, labels, more } = prep(); - set(labels, ['test']); - set(more, { test: 'test' }); - search(); - expect(get(labels)).toBeNull(); - expect(get(more)).toBeNull(); - }); - it('should call ContentNodeResource.fetchCollection if there is no search but a descendant is set', () => { - const { search } = prep({}, ref({ tree_id: 1, lft: 10, rght: 20 })); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - search(); - expect(ContentNodeResource.fetchCollection).toHaveBeenCalledWith({ - getParams: { - tree_id: 1, - lft__gt: 10, - rght__lt: 20, - max_results: 1, - include_coach_content: false, - }, - }); - }); - it('should set labels and clear more if there is no search but a descendant is set', async () => { - const { labels, more, search } = prep({}, ref({ tree_id: 1, lft: 10, rght: 20 })); - const labelsSet = { - available: ['labels'], - channels: [], - languages: [], - }; - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({ labels: labelsSet })); - set(more, { test: 'test' }); - search(); - await Vue.nextTick(); - expect(get(more)).toBeNull(); - expect(get(labels)).toEqual(labelsSet); - }); - it('should call ContentNodeResource.fetchCollection when searchTerms exist', () => { - const { search } = prep({ categories: 'test1,test2' }); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - search(); - expect(ContentNodeResource.fetchCollection).toHaveBeenCalledWith({ - getParams: { - categories: ['test1', 'test2'], - max_results: 25, - include_coach_content: false, - }, - }); - }); - it('should ignore other categories when AllCategories is set and search for isnull false', () => { - const { search } = prep({ categories: `test1,test2,${NoCategories},${AllCategories}` }); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - search(); - expect(ContentNodeResource.fetchCollection).toHaveBeenCalledWith({ - getParams: { categories__isnull: false, max_results: 25, include_coach_content: false }, - }); - }); - it('should ignore other categories when NoCategories is set and search for isnull true', () => { - const { search } = prep({ categories: `test1,test2,${NoCategories}` }); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - search(); - expect(ContentNodeResource.fetchCollection).toHaveBeenCalledWith({ - getParams: { categories__isnull: true, max_results: 25, include_coach_content: false }, - }); - }); - it('should ignore channels when descendant is set', () => { - const { search } = prep( - { - categories: `test1,test2`, - channels: 'test1', - }, - ref({ tree_id: 1, lft: 10, rght: 20 }), - ); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - search(); - expect(ContentNodeResource.fetchCollection).toHaveBeenCalledWith({ - getParams: { - categories: ['test1', 'test2'], - max_results: 25, - tree_id: 1, - lft__gt: 10, - rght__lt: 20, - include_coach_content: false, - }, - }); - }); - it('should set keywords when defined', () => { - const { search } = prep({ keywords: `this is just a test` }); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - search(); - expect(ContentNodeResource.fetchCollection).toHaveBeenCalledWith({ - getParams: { - keywords: `this is just a test`, - max_results: 25, - include_coach_content: false, - }, - }); - }); - it('should set results, labels, and more with returned data', async () => { - const { labels, more, results, search } = prep({ categories: 'test1,test2' }); - const expectedLabels = { - available: ['labels'], - channels: [], - languages: [], - }; - const expectedMore = { - cursor: 'adalskdjsadlkjsadlkjsalkd', - }; - const expectedResults = [{ id: 'node-id1' }]; - ContentNodeResource.fetchCollection.mockReturnValue( - Promise.resolve({ - labels: expectedLabels, - results: expectedResults, - more: expectedMore, - }), - ); - search(); - await Vue.nextTick(); - expect(get(labels)).toEqual(expectedLabels); - expect(get(results)).toEqual(expectedResults); - expect(get(more)).toEqual(expectedMore); - }); - }); - describe('searchMore method', () => { - it('should not call anything when not displaying search terms', () => { - const { searchMore } = prep(); - ContentNodeResource.fetchCollection.mockClear(); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - searchMore(); - expect(ContentNodeResource.fetchCollection).not.toHaveBeenCalled(); - }); - it('should not call anything when more is null', () => { - const { more, searchMore } = prep({ categories: 'test1' }); - ContentNodeResource.fetchCollection.mockClear(); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - set(more, null); - searchMore(); - expect(ContentNodeResource.fetchCollection).not.toHaveBeenCalled(); - }); - it('should not call anything when moreLoading is true', () => { - const { more, moreLoading, searchMore } = prep({ categories: 'test1' }); - ContentNodeResource.fetchCollection.mockClear(); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - set(more, {}); - set(moreLoading, true); - searchMore(); - expect(ContentNodeResource.fetchCollection).not.toHaveBeenCalled(); - }); - it('should pass the more object directly to getParams', () => { - const { more, searchMore } = prep({ categories: `test1,test2,${NoCategories}` }); - ContentNodeResource.fetchCollection.mockReturnValue(Promise.resolve({})); - const moreExpected = { test: 'this', not: 'that' }; - set(more, moreExpected); - searchMore(); - expect(ContentNodeResource.fetchCollection).toHaveBeenCalledWith({ getParams: moreExpected }); - }); - it('should set results, more and labels', async () => { - const { labels, more, results, searchMore, search } = prep({ - categories: `test1,test2,${NoCategories}`, - }); - const expectedLabels = { - available: ['labels'], - channels: [], - languages: [], - }; - const expectedMore = { - cursor: 'adalskdjsadlkjsadlkjsalkd', - }; - const originalResults = [{ id: 'originalId', content_id: 'first' }]; - ContentNodeResource.fetchCollection.mockReturnValue( - Promise.resolve({ - labels: expectedLabels, - results: originalResults, - more: expectedMore, - }), - ); - search(); - await Vue.nextTick(); - const expectedResults = [{ id: 'node-id1', content_id: 'second' }]; - ContentNodeResource.fetchCollection.mockReturnValue( - Promise.resolve({ - labels: expectedLabels, - results: expectedResults, - more: expectedMore, - }), - ); - set(more, {}); - searchMore(); - await Vue.nextTick(); - expect(get(labels)).toEqual(expectedLabels); - expect(get(results)).toEqual(originalResults.concat(expectedResults)); - expect(get(more)).toEqual(expectedMore); - }); - }); - describe('removeFilterTag method', () => { - it('should remove a filter from the searchTerms', () => { - const { removeFilterTag, router } = prep({ - categories: 'test1,test2', - }); - removeFilterTag({ value: 'test1', key: 'categories' }); - expect(router.push).toHaveBeenCalledWith({ - name, - query: { - categories: 'test2', - }, - }); - }); - it('should remove keywords from the searchTerms', () => { - const { removeFilterTag, router } = prep({ - keywords: 'test', - }); - removeFilterTag({ value: 'test', key: 'keywords' }); - expect(router.push).toHaveBeenCalledWith({ - name, - query: {}, - }); - }); - it('should not remove any other filters', () => { - const { removeFilterTag, router } = prep({ - categories: 'test1,test2', - channels: 'channel1', - }); - removeFilterTag({ value: 'test1', key: 'categories' }); - expect(router.push).toHaveBeenCalledWith({ - name, - query: { - categories: 'test2', - channels: 'channel1', - }, - }); - }); - }); - describe('clearSearch method', () => { - it('should remove all filters from the searchTerms', () => { - const { clearSearch, router } = prep({ - categories: 'test1,test2', - channels: 'channel1', - keywords: 'this', - }); - clearSearch(); - expect(router.push).toHaveBeenCalledWith({ - name, - query: {}, - }); - }); - }); -}); diff --git a/kolibri/plugins/learn/assets/src/composables/useLearnerResources.js b/kolibri/plugins/learn/assets/src/composables/useLearnerResources.js index 1f631788068..4c8a669d171 100644 --- a/kolibri/plugins/learn/assets/src/composables/useLearnerResources.js +++ b/kolibri/plugins/learn/assets/src/composables/useLearnerResources.js @@ -11,7 +11,7 @@ import flatMap from 'lodash/flatMap'; import flatMapDepth from 'lodash/flatMapDepth'; import { ContentNodeResource } from 'kolibri.resources'; -import { deduplicateResources } from '../utils/contentNode'; +import { deduplicateResources } from 'kolibri-common/utils/contentNode'; import { LearnerClassroomResource, LearnerLessonResource } from '../apiResources'; import { ClassesPageNames } from '../constants'; import useContentNodeProgress, { setContentNodeProgress } from './useContentNodeProgress'; diff --git a/kolibri/plugins/learn/assets/src/composables/useSearch.js b/kolibri/plugins/learn/assets/src/composables/useSearch.js deleted file mode 100644 index aa5ee5f2527..00000000000 --- a/kolibri/plugins/learn/assets/src/composables/useSearch.js +++ /dev/null @@ -1,482 +0,0 @@ -import { get, set } from '@vueuse/core'; -import invert from 'lodash/invert'; -import logger from 'kolibri.lib.logging'; -import { - computed, - getCurrentInstance, - inject, - provide, - ref, - watch, -} from 'kolibri.lib.vueCompositionApi'; -import { ContentNodeResource } from 'kolibri.resources'; -import { - AllCategories, - Categories, - CategoriesLookup, - ContentLevels, - AccessibilityCategories, - LearningActivities, - NoCategories, - ResourcesNeededTypes, -} from 'kolibri.coreVue.vuex.constants'; -import { deduplicateResources } from '../utils/contentNode'; -import { currentDeviceData } from './useDevices'; -import useContentNodeProgress from './useContentNodeProgress'; -import { setLanguages } from './useLanguages'; - -export const logging = logger.getLogger(__filename); - -const activitiesLookup = invert(LearningActivities); - -function _generateLearningActivitiesShown(learningActivities) { - const learningActivitiesShown = {}; - - (learningActivities || []).map(id => { - const key = activitiesLookup[id]; - learningActivitiesShown[key] = id; - }); - return learningActivitiesShown; -} - -const resourcesNeededShown = [ - 'FOR_BEGINNERS', - 'PEERS', - 'TEACHER', - 'SPECIAL_SOFTWARE', - 'PAPER_PENCIL', - 'INTERNET', - 'OTHER_SUPPLIES', -]; - -function _generateResourcesNeeded(learnerNeeds) { - const resourcesNeeded = {}; - resourcesNeededShown.map(key => { - const value = ResourcesNeededTypes[key]; - if (learnerNeeds && learnerNeeds.includes(value)) { - resourcesNeeded[key] = value; - } - }); - return resourcesNeeded; -} - -const gradeLevelsShown = [ - 'BASIC_SKILLS', - 'PRESCHOOL', - 'LOWER_PRIMARY', - 'UPPER_PRIMARY', - 'LOWER_SECONDARY', - 'UPPER_SECONDARY', - 'TERTIARY', - 'PROFESSIONAL', - 'WORK_SKILLS', -]; - -function _generateGradeLevelsList(gradeLevels) { - return gradeLevelsShown.filter(key => { - return gradeLevels && gradeLevels.includes(ContentLevels[key]); - }); -} - -const accessibilityLabelsShown = [ - 'SIGN_LANGUAGE', - 'AUDIO_DESCRIPTION', - 'TAGGED_PDF', - 'ALT_TEXT', - 'HIGH_CONTRAST', - 'CAPTIONS_SUBTITLES', -]; - -function _generateAccessibilityOptionsList(accessibilityLabels) { - return accessibilityLabelsShown.filter(key => { - return accessibilityLabels && accessibilityLabels.includes(AccessibilityCategories[key]); - }); -} - -function _generateLibraryCategoriesLookup(categories) { - const libraryCategories = {}; - - const availablePaths = {}; - - (categories || []).map(key => { - const paths = key.split('.'); - let path = ''; - for (const path_segment of paths) { - path = path === '' ? path_segment : path + '.' + path_segment; - availablePaths[path] = true; - } - }); - // Create a nested object representing the hierarchy of categories - for (const value of Object.values(Categories) - // Sort by the length of the key path to deal with - // shorter key paths first. - .sort((a, b) => a.length - b.length)) { - // Split the value into the paths so we can build the object - // down the path to create the nested representation - const ids = value.split('.'); - // Start with an empty path - let path = ''; - // Start with the global object - let nested = libraryCategories; - for (const fragment of ids) { - // Add the fragment to create the path we examine - path += fragment; - // Check to see if this path is one of the paths - // that is available on this device - if (availablePaths[path]) { - // Lookup the human readable key for this path - const nestedKey = CategoriesLookup[path]; - // Check if we have already represented this in the object - if (!nested[nestedKey]) { - // If not, add an object representing this category - nested[nestedKey] = { - // The value is the whole path to this point, so the value - // of the key. - value: path, - // Nested is an object that contains any subsidiary categories - nested: {}, - }; - } - // For the next stage of the loop the relevant object to edit is - // the nested object under this key. - nested = nested[nestedKey].nested; - // Add '.' to path so when we next append to the path, - // it is properly '.' separated. - path += '.'; - } else { - break; - } - } - } - return libraryCategories; -} - -export const searchKeys = [ - 'learning_activities', - 'categories', - 'learner_needs', - 'channels', - 'accessibility_labels', - 'languages', - 'grade_levels', -]; - -const { fetchContentNodeProgress } = useContentNodeProgress(); - -export default function useSearch(descendant, store, router) { - // Get store and router references from the curent instance - // but allow them to be passed in to allow for dependency - // injection, primarily for tests. - store = store || getCurrentInstance().proxy.$store; - router = router || getCurrentInstance().proxy.$router; - const route = computed(() => store.state.route); - - const searchResultsLoading = ref(false); - const moreLoading = ref(false); - const _results = ref([]); - const more = ref(null); - const labels = ref(null); - - const { baseurl } = currentDeviceData(store); - - const searchTerms = computed({ - get() { - const searchTerms = {}; - const query = get(route).query; - for (const key of searchKeys) { - const obj = {}; - if (query[key]) { - for (const value of query[key].split(',')) { - obj[value] = true; - } - } - searchTerms[key] = obj; - } - searchTerms.keywords = query.keywords || ''; - return searchTerms; - }, - set(value) { - const query = { ...get(route).query }; - for (const key of searchKeys) { - const val = Object.keys(value[key] || {}) - .filter(Boolean) - .join(','); - if (val.length) { - query[key] = Object.keys(value[key]).join(','); - } else { - delete query[key]; - } - } - if (value.keywords && value.keywords.length) { - query.keywords = value.keywords; - } else { - delete query.keywords; - } - // Just catch an error from making a redundant navigation rather - // than try to precalculate this. - router.push({ ...get(route), query }).catch(() => {}); - }, - }); - - const displayingSearchResults = computed(() => - // Happily this works even for keywords, because calling Object.keys - // on a string value will give an array of the indexes of a string - // for an empty string, this array will be empty, meaning that this - // check still works! - Object.values(get(searchTerms)).some(v => Object.keys(v).length), - ); - - function _setAvailableLabels(searchableLabels) { - if (searchableLabels) { - set(labels, { - ...searchableLabels, - channels: searchableLabels.channels ? searchableLabels.channels.map(c => c.id) : [], - languages: searchableLabels.languages ? searchableLabels.languages.map(l => l.id) : [], - }); - } - } - - function search() { - const currentBaseUrl = get(baseurl); - const getParams = { - include_coach_content: - store.getters.isAdmin || store.getters.isCoach || store.getters.isSuperuser, - baseurl: currentBaseUrl, - }; - const descValue = descendant ? get(descendant) : null; - if (descValue) { - getParams.tree_id = descValue.tree_id; - getParams.lft__gt = descValue.lft; - getParams.rght__lt = descValue.rght; - } - if (get(displayingSearchResults)) { - getParams.max_results = 25; - const terms = get(searchTerms); - set(searchResultsLoading, true); - for (const key of searchKeys) { - if (key === 'categories') { - if (terms[key][AllCategories]) { - getParams['categories__isnull'] = false; - continue; - } else if (terms[key][NoCategories]) { - getParams['categories__isnull'] = true; - continue; - } - } - if (key === 'channels' && descValue) { - continue; - } - const keys = Object.keys(terms[key]); - if (keys.length) { - getParams[key] = keys; - } - } - if (terms.keywords) { - getParams.keywords = terms.keywords; - } - if (store.getters.isUserLoggedIn) { - fetchContentNodeProgress(getParams); - } - ContentNodeResource.fetchCollection({ getParams }).then(data => { - set(_results, data.results || []); - set(more, data.more); - _setAvailableLabels(data.labels); - set(searchResultsLoading, false); - }); - } else if (descValue) { - getParams.max_results = 1; - ContentNodeResource.fetchCollection({ getParams }).then(data => { - _setAvailableLabels(data.labels); - set(more, null); - }); - } else { - // Clear labels if no search results displaying - // and we're not gathering labels from the descendant - set(more, null); - set(labels, null); - } - } - - function searchMore() { - if (get(displayingSearchResults) && get(more) && !get(moreLoading)) { - set(moreLoading, true); - if (store.getters.isUserLoggedIn) { - fetchContentNodeProgress(get(more)); - } - return ContentNodeResource.fetchCollection({ getParams: get(more) }).then(data => { - set(_results, [...get(_results), ...(data.results || [])]); - set(more, data.more); - _setAvailableLabels(data.labels); - set(moreLoading, false); - }); - } - } - - function removeFilterTag({ value, key }) { - if (key === 'keywords') { - set(searchTerms, { - ...get(searchTerms), - [key]: '', - }); - } else { - const keyObject = get(searchTerms)[key]; - delete keyObject[value]; - set(searchTerms, { - ...get(searchTerms), - [key]: keyObject, - }); - } - } - - function clearSearch() { - set(searchTerms, {}); - } - - watch(searchTerms, search); - - if (descendant) { - watch(descendant, newValue => { - if (newValue) { - search(); - } - }); - } - - // Helper to get the route information in a setup() function - function currentRoute() { - return get(route); - } - - const results = computed(() => { - return deduplicateResources(get(_results)); - }); - - // Globally available metadata labels - // These are the labels that are available globally for this search context - // These labels may be disabled for specific searches within a search context - // We use provide/inject here to allow a parent - // component to setup the available labels for child components - // to consume them. - - const globalLabels = ref(null); - - const globalLabelsLoading = ref(false); - - const searchLoading = computed(() => get(searchResultsLoading) || get(globalLabelsLoading)); - - function ensureGlobalLabels() { - set(globalLabelsLoading, true); - const currentBaseUrl = get(baseurl); - ContentNodeResource.fetchCollection({ - getParams: { max_results: 1, baseurl: currentBaseUrl }, - }) - .then(data => { - const labels = data.labels; - set(globalLabels, { - learningActivitiesShown: _generateLearningActivitiesShown(labels.learning_activities), - libraryCategories: _generateLibraryCategoriesLookup(labels.categories), - resourcesNeeded: _generateResourcesNeeded(labels.learner_needs), - gradeLevelsList: _generateGradeLevelsList(labels.grade_levels || []), - accessibilityOptionsList: _generateAccessibilityOptionsList(labels.accessibility_labels), - languagesList: labels.languages || [], - channelsList: labels.channels || [], - }); - setLanguages(labels.languages || []); - }) - .catch(err => logging.error('Failed to fetch search labels from remote', err)) - .then(() => { - set(globalLabelsLoading, false); - }); - } - - ensureGlobalLabels(); - watch(baseurl, ensureGlobalLabels); - - function _getGlobalLabels(name, defaultValue) { - const lookup = get(globalLabels); - if (lookup) { - return lookup[name]; - } - return defaultValue; - } - - const learningActivitiesShown = computed(() => { - return _getGlobalLabels('learningActivitiesShown', {}); - }); - const libraryCategories = computed(() => { - return _getGlobalLabels('libraryCategories', {}); - }); - const resourcesNeeded = computed(() => { - return _getGlobalLabels('resourcesNeeded', {}); - }); - const gradeLevelsList = computed(() => { - return _getGlobalLabels('gradeLevelsList', []); - }); - const accessibilityOptionsList = computed(() => { - return _getGlobalLabels('accessibilityOptionsList', []); - }); - const languagesList = computed(() => { - return _getGlobalLabels('languagesList', []); - }); - const channelsList = computed(() => { - return _getGlobalLabels('channelsList', []); - }); - - provide('availableLearningActivities', learningActivitiesShown); - provide('availableLibraryCategories', libraryCategories); - provide('availableResourcesNeeded', resourcesNeeded); - provide('availableGradeLevels', gradeLevelsList); - provide('availableAccessibilityOptions', accessibilityOptionsList); - provide('availableLanguages', languagesList); - provide('availableChannels', channelsList); - - // Provide an object of searchable labels - // This is a manifest of all the labels that could still be selected and produce search results - // given the currently applied search filters. - provide('searchableLabels', labels); - - // Currently selected search terms - provide('activeSearchTerms', searchTerms); - - return { - currentRoute, - searchTerms, - displayingSearchResults, - searchLoading, - moreLoading, - results, - more, - labels, - search, - searchMore, - removeFilterTag, - clearSearch, - }; -} - -/* - * Helper function to retrieve references for provided properties - * from an ancestor's use of useSearch - */ -export function injectSearch() { - const availableLearningActivities = inject('availableLearningActivities'); - const availableLibraryCategories = inject('availableLibraryCategories'); - const availableResourcesNeeded = inject('availableResourcesNeeded'); - const availableGradeLevels = inject('availableGradeLevels'); - const availableAccessibilityOptions = inject('availableAccessibilityOptions'); - const availableLanguages = inject('availableLanguages'); - const availableChannels = inject('availableChannels'); - const searchableLabels = inject('searchableLabels'); - const activeSearchTerms = inject('activeSearchTerms'); - return { - availableLearningActivities, - availableLibraryCategories, - availableResourcesNeeded, - availableGradeLevels, - availableAccessibilityOptions, - availableLanguages, - availableChannels, - searchableLabels, - activeSearchTerms, - }; -} diff --git a/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue b/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue index 787116ff7ee..932989bb4b3 100644 --- a/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue +++ b/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue @@ -163,7 +163,13 @@ import { get, set } from '@vueuse/core'; - import { onMounted, getCurrentInstance, ref, watch } from 'kolibri.lib.vueCompositionApi'; + import { + onMounted, + getCurrentInstance, + provide, + ref, + watch, + } from 'kolibri.lib.vueCompositionApi'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; import useUser from 'kolibri.coreVue.composables.useUser'; @@ -173,6 +179,7 @@ import MeteredConnectionNotificationModal from 'kolibri-common/components/MeteredConnectionNotificationModal.vue'; import appCapabilities, { checkCapability } from 'kolibri.utils.appCapabilities'; import LearningActivityChip from 'kolibri-common/components/ResourceDisplayAndSearch/LearningActivityChip.vue'; + import useBaseSearch, { searchKeys } from 'kolibri-common/composables/useBaseSearch'; import SidePanelModal from '../SidePanelModal'; import SearchFiltersPanel from '../SearchFiltersPanel'; import { KolibriStudioId, PageNames } from '../../constants'; @@ -185,7 +192,8 @@ setCurrentDevice, StudioNotAllowedError, } from '../../composables/useDevices'; - import useSearch, { searchKeys } from '../../composables/useSearch'; + import useContentNodeProgress from '../../composables/useContentNodeProgress'; + import useLearnerResources from '../../composables/useLearnerResources'; import BrowseResourceMetadata from '../BrowseResourceMetadata'; import commonLearnStrings from '../commonLearnStrings'; @@ -234,6 +242,12 @@ isLearnerOnlyImport, } = useUser(); const { allowDownloadOnMeteredConnection } = useDeviceSettings(); + const { baseurl, deviceName } = currentDeviceData(); + const { fetchContentNodeProgress } = useContentNodeProgress(); + + provide('baseurl', baseurl); + provide('fetchContentNodeProgress', fetchContentNodeProgress); + const { searchTerms, displayingSearchResults, @@ -247,7 +261,7 @@ clearSearch, setCategory, currentRoute, - } = useSearch(); + } = useBaseSearch(); const { resumableContentNodes, moreResumableContentNodes, @@ -260,7 +274,6 @@ const { canAddDownloads, canDownloadExternally } = useCoreLearn(); const { currentCardViewStyle } = useCardViewStyle(); const { back } = useContentLink(); - const { baseurl, deviceName } = currentDeviceData(); const { fetchChannels } = useChannels(); onMounted(() => { diff --git a/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/ActivityButtonsGroup.vue b/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/ActivityButtonsGroup.vue index 4cb406da131..446b1293425 100644 --- a/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/ActivityButtonsGroup.vue +++ b/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/ActivityButtonsGroup.vue @@ -36,13 +36,14 @@ import camelCase from 'lodash/camelCase'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; - import { injectSearch } from '../../composables/useSearch'; + import { injectBaseSearch } from 'kolibri-common/composables/useBaseSearch'; export default { name: 'ActivityButtonsGroup', mixins: [commonCoreStrings], setup() { - const { availableLearningActivities, searchableLabels, activeSearchTerms } = injectSearch(); + const { availableLearningActivities, searchableLabels, activeSearchTerms } = + injectBaseSearch(); return { availableLearningActivities, searchableLabels, diff --git a/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/CategorySearchModal/CategorySearchOptions.vue b/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/CategorySearchModal/CategorySearchOptions.vue index 7fa164e0c5c..13a3f88ef8a 100644 --- a/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/CategorySearchModal/CategorySearchOptions.vue +++ b/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/CategorySearchModal/CategorySearchOptions.vue @@ -59,13 +59,14 @@ import camelCase from 'lodash/camelCase'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; - import { injectSearch } from '../../../composables/useSearch'; + import { injectBaseSearch } from 'kolibri-common/composables/useBaseSearch'; export default { name: 'CategorySearchOptions', mixins: [commonCoreStrings], setup() { - const { activeSearchTerms, availableLibraryCategories, searchableLabels } = injectSearch(); + const { activeSearchTerms, availableLibraryCategories, searchableLabels } = + injectBaseSearch(); return { activeSearchTerms, availableLibraryCategories, diff --git a/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/SelectGroup.vue b/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/SelectGroup.vue index 255d42cf2cf..419bc994fd7 100644 --- a/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/SelectGroup.vue +++ b/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/SelectGroup.vue @@ -58,7 +58,7 @@ import camelCase from 'lodash/camelCase'; import { ContentLevels, AccessibilityCategories } from 'kolibri.coreVue.vuex.constants'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; - import { injectSearch } from '../../composables/useSearch'; + import { injectBaseSearch } from 'kolibri-common/composables/useBaseSearch'; export default { name: 'SelectGroup', @@ -70,7 +70,7 @@ availableLanguages, availableChannels, searchableLabels, - } = injectSearch(); + } = injectBaseSearch(); return { availableGradeLevels, availableAccessibilityOptions, diff --git a/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/index.vue b/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/index.vue index 36085459b11..2630965fa51 100644 --- a/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/index.vue +++ b/kolibri/plugins/learn/assets/src/views/SearchFiltersPanel/index.vue @@ -130,11 +130,11 @@ import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; import { ref } from 'kolibri.lib.vueCompositionApi'; + import { injectBaseSearch } from 'kolibri-common/composables/useBaseSearch'; import SearchBox from '../SearchBox'; import SidePanelModal from '../SidePanelModal'; import commonLearnStrings from '../commonLearnStrings'; import useContentLink from '../../composables/useContentLink'; - import { injectSearch } from '../../composables/useSearch'; import ActivityButtonsGroup from './ActivityButtonsGroup'; import CategorySearchModal from './CategorySearchModal'; import SelectGroup from './SelectGroup'; @@ -157,7 +157,7 @@ availableResourcesNeeded, searchableLabels, activeSearchTerms, - } = injectSearch(); + } = injectBaseSearch(); const currentCategory = ref(null); return { availableLibraryCategories, diff --git a/kolibri/plugins/learn/assets/src/views/TopicsPage/index.vue b/kolibri/plugins/learn/assets/src/views/TopicsPage/index.vue index c2247bb820a..5f0de7de0d5 100644 --- a/kolibri/plugins/learn/assets/src/views/TopicsPage/index.vue +++ b/kolibri/plugins/learn/assets/src/views/TopicsPage/index.vue @@ -263,7 +263,7 @@ import lodashSet from 'lodash/set'; import lodashGet from 'lodash/get'; import KBreadcrumbs from 'kolibri-design-system/lib/KBreadcrumbs'; - import { getCurrentInstance, ref, watch } from 'kolibri.lib.vueCompositionApi'; + import { getCurrentInstance, ref, watch, provide } from 'kolibri.lib.vueCompositionApi'; import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; import useUser from 'kolibri.coreVue.composables.useUser'; import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants'; @@ -274,14 +274,18 @@ import { ContentNodeResource } from 'kolibri.resources'; import plugin_data from 'plugin_data'; import LearningActivityChip from 'kolibri-common/components/ResourceDisplayAndSearch/LearningActivityChip.vue'; + import useBaseSearch from 'kolibri-common/composables/useBaseSearch'; import SidePanelModal from '../SidePanelModal'; import { PageNames } from '../../constants'; import useChannels from '../../composables/useChannels'; - import useSearch from '../../composables/useSearch'; import useContentLink from '../../composables/useContentLink'; import useContentNodeProgress from '../../composables/useContentNodeProgress'; import useCoreLearn from '../../composables/useCoreLearn'; - import { setCurrentDevice, StudioNotAllowedError } from '../../composables/useDevices'; + import { + setCurrentDevice, + StudioNotAllowedError, + currentDeviceData, + } from '../../composables/useDevices'; import useDownloadRequests from '../../composables/useDownloadRequests'; import LibraryAndChannelBrowserMainContent from '../LibraryAndChannelBrowserMainContent'; import SearchFiltersPanel from '../SearchFiltersPanel'; @@ -363,6 +367,12 @@ const store = currentInstance.$store; const router = currentInstance.$router; const topic = ref(null); + const { fetchContentNodeProgress, fetchContentNodeTreeProgress } = useContentNodeProgress(); + const { baseurl } = currentDeviceData(); + + provide('fetchContentNodeProgress', fetchContentNodeProgress); + provide('baseurl', baseurl); + const { searchTerms, displayingSearchResults, @@ -375,11 +385,10 @@ removeFilterTag, clearSearch, currentRoute, - } = useSearch(topic); + } = useBaseSearch(topic); const { back, genContentLinkKeepCurrentBackLink } = useContentLink(); const { windowBreakpoint, windowIsLarge, windowIsSmall } = useKResponsiveWindow(); const { channelsMap, fetchChannels } = useChannels(); - const { fetchContentNodeProgress, fetchContentNodeTreeProgress } = useContentNodeProgress(); const { isUserLoggedIn, isCoach, isAdmin, isSuperuser } = useUser(); const { fetchUserDownloadRequests } = useDownloadRequests(store); diff --git a/kolibri/plugins/learn/assets/test/views/library-page.spec.js b/kolibri/plugins/learn/assets/test/views/library-page.spec.js index 47ad7e51573..340dd56913d 100644 --- a/kolibri/plugins/learn/assets/test/views/library-page.spec.js +++ b/kolibri/plugins/learn/assets/test/views/library-page.spec.js @@ -6,11 +6,11 @@ import KCircularLoader from 'kolibri-design-system/lib/loaders/KCircularLoader'; import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow'; import { ContentNodeResource } from 'kolibri.resources'; import useUser from 'kolibri.coreVue.composables.useUser'; +import useBaseSearch, { useBaseSearchMock } from 'kolibri-common/composables/useBaseSearch'; import { PageNames } from '../../src/constants'; import LibraryPage from '../../src/views/LibraryPage'; import OtherLibraries from '../../src/views/LibraryPage/OtherLibraries'; /* eslint-disable import/named */ -import useSearch, { useSearchMock } from '../../src/composables/useSearch'; import useChannels, { useChannelsMock } from '../../src/composables/useChannels'; import usePinnedDevices, { usePinnedDevicesMock } from '../../src/composables/usePinnedDevices'; import useDevices, { useDevicesMock } from '../../src/composables/useDevices'; @@ -39,7 +39,7 @@ const CHANNEL = { jest.mock('../../src/composables/useChannels'); jest.mock('../../src/composables/useCardLayoutSpan'); jest.mock('../../src/composables/useDevices'); -jest.mock('../../src/composables/useSearch'); +jest.mock('../../src/composables/useBaseSearch'); jest.mock('../../src/composables/useLearnerResources'); jest.mock('../../src/composables/useLearningActivities'); jest.mock('../../src/composables/useContentLink'); @@ -147,9 +147,9 @@ describe('LibraryPage', () => { describe('displaying channels and recent/popular content ', () => { beforeAll(() => { - useSearch.mockImplementation(() => useSearchMock({ displayingSearchResults: false })); + useBaseSearch.mockImplementation(() => useBaseSearchMock({ displayingSearchResults: false })); }); - /** useSearch#displayingSearchResults is falsy and there are rootNodes */ + /** useBaseSearch#displayingSearchResults is falsy and there are rootNodes */ it('displays a grid of channel cards', async () => { const wrapper = await makeWrapper(); expect(wrapper.find('[data-test="channels"').element).toBeTruthy(); @@ -164,7 +164,7 @@ describe('LibraryPage', () => { describe('when page is loading', () => { it('shows a KCircularLoader', async () => { - useSearch.mockImplementation(() => useSearchMock({ searchLoading: true })); + useBaseSearch.mockImplementation(() => useBaseSearchMock({ searchLoading: true })); const wrapper = await makeWrapper(); expect(wrapper.findComponent(KCircularLoader).exists()).toBeTruthy(); }); @@ -172,7 +172,7 @@ describe('LibraryPage', () => { describe('nothing in library label', () => { beforeAll(() => { - useSearch.mockImplementation(() => useSearchMock({ displayingSearchResults: false })); + useBaseSearch.mockImplementation(() => useBaseSearchMock({ displayingSearchResults: false })); }); it('display when no channels are available', async () => { const wrapper = await makeWrapper({ rootNodes: [] }); @@ -189,7 +189,7 @@ describe('LibraryPage', () => { describe('Resumable content', () => { beforeAll(() => { - useSearch.mockImplementation(() => useSearchMock({ displayingSearchResults: false })); + useBaseSearch.mockImplementation(() => useBaseSearchMock({ displayingSearchResults: false })); }); it('show content', async () => { const wrapper = await makeWrapper(); @@ -233,7 +233,7 @@ describe('LibraryPage', () => { } beforeEach(() => { useUser.mockImplementation(() => ({ isUserLoggedIn: true })); - useSearch.mockImplementation(() => useSearchMock({ displayingSearchResults: false })); + useBaseSearch.mockImplementation(() => useBaseSearchMock({ displayingSearchResults: false })); }); it('show other libraries', async () => { @@ -339,7 +339,7 @@ describe('LibraryPage', () => { describe('SearchResultsGrid', () => { beforeEach(() => { - useSearch.mockImplementation(() => useSearchMock({ displayingSearchResults: true })); + useBaseSearch.mockImplementation(() => useBaseSearchMock({ displayingSearchResults: true })); }); it('display search results grid', async () => { const wrapper = await makeWrapper(); diff --git a/kolibri/plugins/learn/assets/test/views/resumable-content-grid.spec.js b/kolibri/plugins/learn/assets/test/views/resumable-content-grid.spec.js index 1acc16a08ea..03bec517c7a 100644 --- a/kolibri/plugins/learn/assets/test/views/resumable-content-grid.spec.js +++ b/kolibri/plugins/learn/assets/test/views/resumable-content-grid.spec.js @@ -15,7 +15,7 @@ describe('when there are nodes with progress that can be resumed', () => { /* * moreResumableContentNodes existing would realistically mean * that there are 13+ resumableContentNodes but we - * rely on useSearch to handle the details of that implementation + * rely on useBaseSearch to handle the details of that implementation */ resumableContentNodes: [ { node: 1 }, diff --git a/kolibri/plugins/learn/assets/test/views/search-results-grid.spec.js b/kolibri/plugins/learn/assets/test/views/search-results-grid.spec.js index fc1d833adc5..edd1de3d95b 100644 --- a/kolibri/plugins/learn/assets/test/views/search-results-grid.spec.js +++ b/kolibri/plugins/learn/assets/test/views/search-results-grid.spec.js @@ -6,10 +6,10 @@ import SearchResultsGrid from '../../src/views/SearchResultsGrid.vue'; const SearchStrings = createTranslator('SearchResultsGrid', SearchResultsGrid.$trs); const coreStrings = commonCoreStrings.methods.coreString; -jest.mock('../../src/composables/useSearch'); +jest.mock('../../src/composables/useBaseSearch'); describe('when search results are loaded', () => { - /* useSearch#displayingSearchResults is truthy and isLoading is false */ + /* useBaseSearch#displayingSearchResults is truthy and isLoading is false */ it('does not display a list of channels', () => { const wrapper = shallowMount(SearchResultsGrid, {}); expect(wrapper.findComponent({ name: 'ChannelCardGroupGrid' }).exists()).toBe(false); @@ -31,7 +31,7 @@ describe('when search results are loaded', () => { describe('when there are no more results to show than the default amount', () => { it('displays results message with the number of results', () => { - /* useSearch#more is relied on here to determine if there are more to show */ + /* useBaseSearch#more is relied on here to determine if there are more to show */ const wrapper = shallowMount(SearchResultsGrid, { propsData: { results: [{ result: 1 }], diff --git a/kolibri/plugins/learn/assets/test/views/topics-page.spec.js b/kolibri/plugins/learn/assets/test/views/topics-page.spec.js index e4449cf5e47..f8176dad046 100644 --- a/kolibri/plugins/learn/assets/test/views/topics-page.spec.js +++ b/kolibri/plugins/learn/assets/test/views/topics-page.spec.js @@ -12,7 +12,7 @@ import CustomContentRenderer from '../../src/views/ChannelRenderer/CustomContent import { PageNames } from '../../src/constants'; import TopicsPage from '../../src/views/TopicsPage'; // eslint-disable-next-line import/named -import useSearch, { useSearchMock } from '../../src/composables/useSearch'; +import useBaseSearch, { useBaseSearchMock } from '../../src/composables/useBaseSearch'; // eslint-disable-next-line import/named import useChannels, { useChannelsMock } from '../../src/composables/useChannels'; @@ -88,7 +88,7 @@ jest.mock('kolibri.resources'); jest.mock('kolibri.urls'); jest.mock('kolibri.coreVue.composables.useUser'); jest.mock('../../src/composables/useContentLink'); -jest.mock('../../src/composables/useSearch'); +jest.mock('../../src/composables/useBaseSearch'); jest.mock('../../src/composables/useChannels'); // Needed to test anything using mount() where children use this composable jest.mock('../../src/composables/useLearningActivities'); @@ -108,8 +108,8 @@ describe('TopicsPage', () => { let store; beforeEach(() => { - useSearch.mockImplementation(() => - useSearchMock({ + useBaseSearch.mockImplementation(() => + useBaseSearchMock({ displayingSearchResults: false, results: [], search: jest.fn(), @@ -153,7 +153,7 @@ describe('TopicsPage', () => { it('renders a CustomContentRenderer', async () => { plugin_data.enableCustomChannelNav.mockImplementation(() => true); - useSearch.mockImplementation(() => useSearchMock()); + useBaseSearch.mockImplementation(() => useBaseSearchMock()); ContentNodeResource.fetchTree.mockResolvedValue({ ...DEFAULT_TOPIC, @@ -275,8 +275,8 @@ describe('TopicsPage', () => { jest.clearAllMocks(); - useSearch.mockImplementation(() => - useSearchMock({ + useBaseSearch.mockImplementation(() => + useBaseSearchMock({ displayingSearchResults: true, // true here results, // those we just made above search: jest.fn(), @@ -309,8 +309,8 @@ describe('TopicsPage', () => { it('displays a grid of cards with the topics and their chidlren', async () => { jest.clearAllMocks(); - useSearch.mockImplementation(() => - useSearchMock({ + useBaseSearch.mockImplementation(() => + useBaseSearchMock({ displayingSearchResults: false, results: [], search: jest.fn(), @@ -387,8 +387,8 @@ describe('TopicsPage', () => { it('shows correct breadcrumbs at a non-Channel Topic', async () => { ContentNodeResource.fetchTree.mockResolvedValue(DEFAULT_TOPIC.children.results[0]); - useSearch.mockImplementation(() => - useSearchMock({ + useBaseSearch.mockImplementation(() => + useBaseSearchMock({ displayingSearchResults: false, results: [], search: jest.fn(), diff --git a/packages/kolibri-common/composables/__mocks__/useBaseSearch.js b/packages/kolibri-common/composables/__mocks__/useBaseSearch.js index ba904503ab8..527e3017c22 100644 --- a/packages/kolibri-common/composables/__mocks__/useBaseSearch.js +++ b/packages/kolibri-common/composables/__mocks__/useBaseSearch.js @@ -73,7 +73,7 @@ export function useBaseSearch(overrides = {}) { export default jest.fn(() => useBaseSearch()); -export const injectSearch = jest.fn(() => ({ +export const injectBaseSearch = jest.fn(() => ({ availableLearningActivities: [], availableLibraryCategories: [], availableResourcesNeeded: [], diff --git a/packages/kolibri-common/composables/useBaseSearch.js b/packages/kolibri-common/composables/useBaseSearch.js index da5e4284094..e9c2babd51d 100644 --- a/packages/kolibri-common/composables/useBaseSearch.js +++ b/packages/kolibri-common/composables/useBaseSearch.js @@ -20,12 +20,8 @@ import { NoCategories, ResourcesNeededTypes, } from 'kolibri.coreVue.vuex.constants'; -/* - * TBD #12517 + import { deduplicateResources } from '../utils/contentNode'; -import { currentDeviceData } from './useDevices'; -import useContentNodeProgress from './useContentNodeProgress'; -*/ import { setLanguages } from './useLanguages'; export const logging = logger.getLogger(__filename); @@ -164,10 +160,13 @@ export const searchKeys = [ 'grade_levels', ]; -/* - * TBD #12517 -const { fetchContentNodeProgress } = useContentNodeProgress(); -*/ +// const getBaseurl = () => { +// try { +// return inject("baseurl"); +// } catch { +// return null; +// } +// } export default function useBaseSearch(descendant, store, router) { // Get store and router references from the curent instance @@ -183,10 +182,8 @@ export default function useBaseSearch(descendant, store, router) { const more = ref(null); const labels = ref(null); - /* - * TBD #12517 - const { baseurl } = currentDeviceData(store); - */ + const baseurl = inject('baseurl', null); + const fetchContentNodeProgress = inject('fetchContentNodeProgress', null); const searchTerms = computed({ get() { @@ -246,7 +243,7 @@ export default function useBaseSearch(descendant, store, router) { } function search() { - const currentBaseUrl = get(baseurl); + const currentBaseUrl = baseurl && get(baseurl); const getParams = { include_coach_content: store.getters.isAdmin || store.getters.isCoach || store.getters.isSuperuser, @@ -284,7 +281,7 @@ export default function useBaseSearch(descendant, store, router) { getParams.keywords = terms.keywords; } if (store.getters.isUserLoggedIn) { - fetchContentNodeProgress(getParams); + fetchContentNodeProgress?.(getParams); } ContentNodeResource.fetchCollection({ getParams }).then(data => { set(_results, data.results || []); @@ -310,7 +307,7 @@ export default function useBaseSearch(descendant, store, router) { if (get(displayingSearchResults) && get(more) && !get(moreLoading)) { set(moreLoading, true); if (store.getters.isUserLoggedIn) { - fetchContentNodeProgress(get(more)); + fetchContentNodeProgress?.(get(more)); } return ContentNodeResource.fetchCollection({ getParams: get(more) }).then(data => { set(_results, [...get(_results), ...(data.results || [])]); @@ -375,7 +372,7 @@ export default function useBaseSearch(descendant, store, router) { function ensureGlobalLabels() { set(globalLabelsLoading, true); - const currentBaseUrl = get(baseurl); + const currentBaseUrl = baseurl && get(baseurl); ContentNodeResource.fetchCollection({ getParams: { max_results: 1, baseurl: currentBaseUrl }, }) @@ -399,7 +396,9 @@ export default function useBaseSearch(descendant, store, router) { } ensureGlobalLabels(); - watch(baseurl, ensureGlobalLabels); + if (baseurl) { + watch(baseurl, ensureGlobalLabels); + } function _getGlobalLabels(name, defaultValue) { const lookup = get(globalLabels); @@ -467,7 +466,7 @@ export default function useBaseSearch(descendant, store, router) { * Helper function to retrieve references for provided properties * from an ancestor's use of useBaseSearch */ -export function injectSearch() { +export function injectBaseSearch() { const availableLearningActivities = inject('availableLearningActivities'); const availableLibraryCategories = inject('availableLibraryCategories'); const availableResourcesNeeded = inject('availableResourcesNeeded'); diff --git a/kolibri/plugins/learn/assets/src/utils/contentNode.js b/packages/kolibri-common/utils/contentNode.js similarity index 100% rename from kolibri/plugins/learn/assets/src/utils/contentNode.js rename to packages/kolibri-common/utils/contentNode.js