Skip to content

Commit c74f49d

Browse files
committed
Allow application of a primary language filter to shape all library browsing.
1 parent 146e952 commit c74f49d

21 files changed

Lines changed: 397 additions & 157 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import useFetch from './useFetch';
5353
* @property {(resources: Array<Object>) => void} setSelectedResources Replaces the current
5454
* `selectedResources` array with the provided resources array.
5555
* @property {() => void} clearSearch Clears the current search terms and results.
56-
* @property {(tag: Object) => void} removeSearchFilterTag Removes the specified tag from the
56+
* @property {(tag: Object) => void} removeSearchTerm Removes the specified term from the
5757
* search terms.
5858
*
5959
* @returns {UseResourceSelectionResponse}
@@ -250,6 +250,6 @@ export default function useResourceSelection({
250250
deselectResources,
251251
setSelectedResources,
252252
clearSearch: useSearchObject.clearSearch,
253-
removeSearchFilterTag: useSearchObject.removeFilterTag,
253+
removeSearchTerm: useSearchObject.removeSearchTerm,
254254
};
255255
}

kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/index.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
@selectResources="selectResources"
4949
@deselectResources="deselectResources"
5050
@setSelectedResources="setSelectedResources"
51-
@removeSearchFilterTag="removeSearchFilterTag"
51+
@removeSearchTerm="removeSearchTerm"
5252
/>
5353

5454
<template
@@ -146,7 +146,7 @@
146146
selectResources,
147147
deselectResources,
148148
setSelectedResources,
149-
removeSearchFilterTag,
149+
removeSearchTerm,
150150
} = useResourceSelection({
151151
searchResultsRouteName: PageNames.LESSON_SELECT_RESOURCES_SEARCH_RESULTS,
152152
});
@@ -231,7 +231,7 @@
231231
setSelectedResources,
232232
notifyResourcesAdded,
233233
notifySaveLessonError,
234-
removeSearchFilterTag,
234+
removeSearchTerm,
235235
goBackAction$,
236236
cancelAction$,
237237
continueAction$,

kolibri/plugins/coach/assets/src/views/lessons/LessonSummaryPage/sidePanels/LessonResourceSelection/subPages/SelectFromSearchResults.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<SearchChips
2020
class="mb-16"
2121
:searchTerms="searchTerms"
22-
@removeItem="onRemoveSearchFilterTag"
22+
@removeItem="onRemoveSearchTerm"
2323
@clearSearch="onClearSearch"
2424
/>
2525

@@ -211,8 +211,8 @@
211211
this.$emit('clearSearch');
212212
this.redirectBack();
213213
},
214-
onRemoveSearchFilterTag(item, { isLast }) {
215-
this.$emit('removeSearchFilterTag', item);
214+
onRemoveSearchTerm(item, { isLast }) {
215+
this.$emit('removeSearchTerm', item);
216216
if (isLast) {
217217
this.redirectBack();
218218
}

kolibri/plugins/learn/assets/src/composables/useContentLink.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { get } from '@vueuse/core';
22
import isEmpty from 'lodash/isEmpty';
33
import pick from 'lodash/pick';
44
import { computed, getCurrentInstance } from 'vue';
5+
import { primaryLanguageKey } from 'kolibri-common/composables/useBaseSearch';
56
import { ExternalPagePaths, PageNames } from '../constants';
67

78
function _decodeBackLinkQuery(query) {
@@ -15,8 +16,22 @@ export default function useContentLink(store) {
1516
store = store || getCurrentInstance().proxy.$store;
1617
const route = computed(() => store.state.route);
1718

19+
function _setPrimaryLanguageKey(query) {
20+
if (query[primaryLanguageKey]) {
21+
return query;
22+
}
23+
const oldQuery = get(route).query || {};
24+
if (oldQuery[primaryLanguageKey]) {
25+
query[primaryLanguageKey] = oldQuery[primaryLanguageKey];
26+
}
27+
return query;
28+
}
29+
1830
function _makeNodeLink(id, isResource, query, deviceId) {
1931
const params = get(route).params;
32+
if (!isResource) {
33+
query = _setPrimaryLanguageKey(query);
34+
}
2035
return {
2136
name: isResource ? PageNames.TOPICS_CONTENT : PageNames.TOPICS_TOPIC,
2237
params: pick({ id, deviceId: deviceId || params.deviceId }, ['id', 'deviceId']),
@@ -152,7 +167,8 @@ export default function useContentLink(store) {
152167
return null;
153168
}
154169

155-
const query = _getBackLinkQuery();
170+
const query = _setPrimaryLanguageKey(_getBackLinkQuery());
171+
156172
return {
157173
name: PageNames.LIBRARY,
158174
params: { deviceId },

kolibri/plugins/learn/assets/src/composables/useDevices.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import useChannels from 'kolibri-common/composables/useChannels';
1313
import plugin_data from 'kolibri-plugin-data';
1414
import { KolibriStudioId } from '../constants';
1515
import { learnStrings } from '../views/commonLearnStrings';
16+
import { sortChannels } from '../utils/sortChannels';
1617

1718
/**
1819
* The ref is defined in the outer scope so it can be used as a shared store
@@ -110,7 +111,13 @@ export function currentDeviceData(store) {
110111
};
111112
}
112113

113-
export default function useDevices(store) {
114+
/**
115+
* A composable function to access data about devices and their channels.
116+
*
117+
* @param {Ref<string|null>} primaryLanguage - The primary language as a VueJS ref.
118+
* @param {Object} store - The Vuex store instance.
119+
*/
120+
export default function useDevices(primaryLanguage = null, store) {
114121
const { fetchChannels } = useChannels();
115122
const networkDevices = ref({});
116123
const isLoading = ref(false);
@@ -122,7 +129,7 @@ export default function useDevices(store) {
122129
function _updateDeviceChannels(device, channels) {
123130
set(deviceChannelsMap, {
124131
...get(deviceChannelsMap),
125-
[device.instance_id]: channels,
132+
[device.instance_id]: sortChannels(channels, get(primaryLanguage)),
126133
});
127134
}
128135

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import orderBy from 'lodash/orderBy';
2+
import { getContentLangActive } from 'kolibri/utils/i18n';
3+
4+
export function sortChannels(channels, primaryLanguage) {
5+
return orderBy(
6+
channels,
7+
[
8+
c => getContentLangActive(c.lang || c.lang_code, primaryLanguage),
9+
c => Math.max(...c.included_languages.map(l => getContentLangActive(l, primaryLanguage))),
10+
],
11+
['desc', 'desc'],
12+
);
13+
}

kolibri/plugins/learn/assets/src/views/LibraryPage/OtherLibraries.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
import { computed } from 'vue';
146146
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
147147
import { coreStrings } from 'kolibri/uiText/commonCoreStrings';
148+
import { injectBaseSearch } from 'kolibri-common/composables/useBaseSearch';
148149
import useCardLayoutSpan from '../../composables/useCardLayoutSpan';
149150
import useContentLink from '../../composables/useContentLink';
150151
import useDevices from '../../composables/useDevices';
@@ -161,12 +162,13 @@
161162
UnPinnedDevices,
162163
},
163164
setup() {
165+
const { currentPrimaryLanguageId } = injectBaseSearch();
164166
const {
165167
isLoadingChannels,
166168
networkDevicesWithChannels,
167169
keepDeviceChannelsUpdated,
168170
deviceChannelsMap,
169-
} = useDevices();
171+
} = useDevices(currentPrimaryLanguageId);
170172
const {
171173
handlePinToggle,
172174
fetchPinsForUser,

kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,16 @@
4949
v-else-if="!displayingSearchResults && !rootNodesLoading"
5050
data-test="channels"
5151
>
52-
<h1 class="channels-label">
53-
{{ channelsLabel }}
54-
</h1>
52+
<KFixedGrid numCols="4">
53+
<KFixedGridItem span="3">
54+
<h1 class="channels-label">
55+
{{ channelsLabel }}
56+
</h1>
57+
</KFixedGridItem>
58+
<KFixedGridItem span="1">
59+
<LanguageSelector :primary="true" />
60+
</KFixedGridItem>
61+
</KFixedGrid>
5562
<p
5663
v-if="isLocalLibraryEmpty"
5764
data-test="nothing-in-lib-label"
@@ -88,7 +95,6 @@
8895
data-test="search-results"
8996
:allowDownloads="allowDownloads"
9097
:results="results"
91-
:removeFilterTag="removeFilterTag"
9298
:clearSearch="clearSearch"
9399
:moreLoading="moreLoading"
94100
:searchMore="searchMore"
@@ -109,6 +115,7 @@
109115
:class="windowIsLarge ? 'side-panel' : ''"
110116
data-test="side-panel-local"
111117
:width="`${sidePanelWidth}px`"
118+
:showLanguages="displayingSearchResults"
112119
/>
113120
</div>
114121

@@ -122,6 +129,7 @@
122129
v-model="searchTerms"
123130
data-test="side-panel"
124131
:width="`${sidePanelWidth}px`"
132+
:showLanguages="displayingSearchResults"
125133
/>
126134
</SidePanelModal>
127135

@@ -187,9 +195,10 @@
187195
import MeteredConnectionNotificationModal from 'kolibri-common/components/MeteredConnectionNotificationModal.vue';
188196
import appCapabilities, { checkCapability } from 'kolibri/utils/appCapabilities';
189197
import LearningActivityChip from 'kolibri-common/components/ResourceDisplayAndSearch/LearningActivityChip.vue';
190-
import { searchKeys } from 'kolibri-common/composables/useBaseSearch';
191198
import SidePanelModal from 'kolibri-common/components/SidePanelModal';
192199
import SearchFiltersPanel from 'kolibri-common/components/SearchFiltersPanel';
200+
import LanguageSelector from 'kolibri-common/components/SearchFiltersPanel/LanguageSelector';
201+
import { allLanguagesValue } from 'kolibri-common/composables/useBaseSearch';
193202
import useChannels from 'kolibri-common/composables/useChannels';
194203
import { KolibriStudioId, PageNames } from '../../constants';
195204
import useCardViewStyle from '../../composables/useCardViewStyle';
@@ -203,6 +212,7 @@
203212
} from '../../composables/useDevices';
204213
import useSearch from '../../composables/useSearch';
205214
import useLearnerResources from '../../composables/useLearnerResources';
215+
import { sortChannels } from '../../utils/sortChannels';
206216
import BrowseResourceMetadata from '../BrowseResourceMetadata';
207217
import commonLearnStrings from '../commonLearnStrings';
208218
import ChannelCardGroupGrid from '../ChannelCardGroupGrid';
@@ -233,21 +243,15 @@
233243
LearnAppBarPage,
234244
OtherLibraries,
235245
PostSetupModalGroup,
246+
LanguageSelector,
236247
},
237248
mixins: [commonLearnStrings, commonCoreStrings],
238249
setup(props) {
239250
const currentInstance = getCurrentInstance().proxy;
240251
const store = currentInstance.$store;
241252
const router = currentInstance.$router;
242253
243-
const {
244-
isUserLoggedIn,
245-
isCoach,
246-
isAdmin,
247-
isSuperuser,
248-
canManageContent,
249-
isLearnerOnlyImport,
250-
} = useUser();
254+
const { isUserLoggedIn, canManageContent, isLearnerOnlyImport } = useUser();
251255
const { allowDownloadOnMeteredConnection } = useDeviceSettings();
252256
const {
253257
searchTerms,
@@ -258,9 +262,10 @@
258262
more,
259263
search,
260264
searchMore,
261-
removeFilterTag,
262265
clearSearch,
263266
currentRoute,
267+
createBaseSearchGetParams,
268+
primaryLanguage,
264269
} = useSearch();
265270
search();
266271
const { fetchResumableContentNodes } = useLearnerResources();
@@ -288,32 +293,42 @@
288293
fetchResumableContentNodes();
289294
}
290295
const shouldResolve = samePageCheckGenerator(store);
296+
const getParams = createBaseSearchGetParams();
297+
getParams.parent__isnull = true;
291298
return ContentNodeResource.fetchCollection({
292-
getParams: {
293-
parent__isnull: true,
294-
include_coach_content: get(isAdmin) || get(isCoach) || get(isSuperuser),
295-
baseurl,
296-
},
299+
getParams,
297300
}).then(
298301
channelCollection => {
299302
if (shouldResolve()) {
300303
// we want them to be in the same order as the channels list
301304
set(
302305
rootNodes,
303-
channels
304-
.map(channel => {
305-
const node = channelCollection.find(n => n.channel_id === channel.id);
306-
if (node) {
307-
// The `channel` comes with additional data that is
308-
// not returned from the ContentNodeResource.
309-
// Namely thumbnail, description and tagline (so far)
310-
node.title = channel.name || node.title;
311-
node.thumbnail = channel.thumbnail;
312-
node.description = channel.tagline || channel.description;
313-
return node;
314-
}
315-
})
316-
.filter(Boolean),
306+
// Sort channels by relevance to the current user interface language
307+
sortChannels(
308+
channels
309+
.map(channel => {
310+
const node = channelCollection.find(n => n.channel_id === channel.id);
311+
if (node) {
312+
// The `channel` comes with additional data that is
313+
// not returned from the ContentNodeResource.
314+
// Namely thumbnail, description and tagline (so far)
315+
node.title = channel.name || node.title;
316+
node.thumbnail = channel.thumbnail;
317+
node.description = channel.tagline || channel.description;
318+
node.included_languages = channel.included_languages;
319+
return node;
320+
}
321+
})
322+
// Filter out any channels that are not in the user's primary language
323+
// we have filtered this in the initial API fetch, but for older versions
324+
// of Kolibri, the API filter will be a noop.
325+
.filter(
326+
c =>
327+
(c && get(primaryLanguage) === allLanguagesValue) ||
328+
c.included_languages.includes(get(primaryLanguage)),
329+
),
330+
get(primaryLanguage) === allLanguagesValue ? null : get(primaryLanguage),
331+
),
317332
);
318333
319334
store.commit('CORE_SET_PAGE_LOADING', false);
@@ -332,7 +347,8 @@
332347
}
333348
334349
function _showLibrary(baseurl) {
335-
return fetchChannels({ baseurl }).then(channels => {
350+
const params = createBaseSearchGetParams();
351+
return fetchChannels(params).then(channels => {
336352
if (!channels.length && isUserLoggedIn) {
337353
router.replace({ name: PageNames.CONTENT_UNAVAILABLE });
338354
return;
@@ -342,9 +358,7 @@
342358
return;
343359
}
344360
345-
const query = currentRoute().query;
346-
347-
if (searchKeys.some(key => query[key])) {
361+
if (get(displayingSearchResults)) {
348362
// If currently on a route with search terms
349363
// just finish early and let the component handle loading
350364
store.commit('CORE_SET_PAGE_LOADING', false);
@@ -386,6 +400,12 @@
386400
}
387401
});
388402
403+
watch(primaryLanguage, () => {
404+
if (!displayingSearchResults.value) {
405+
showLibrary();
406+
}
407+
});
408+
389409
showLibrary();
390410
391411
return {
@@ -399,7 +419,6 @@
399419
results,
400420
more,
401421
searchMore,
402-
removeFilterTag,
403422
clearSearch,
404423
windowBreakpoint,
405424
windowIsLarge,

0 commit comments

Comments
 (0)