Skip to content

Commit

Permalink
Implement a channel language filter.
Browse files Browse the repository at this point in the history
  • Loading branch information
rtibbles committed Nov 27, 2024
1 parent 7a44415 commit bb22259
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 11 deletions.
103 changes: 96 additions & 7 deletions kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,22 @@
v-else-if="!displayingSearchResults && !rootNodesLoading"
data-test="channels"
>
<h1 class="channels-label">
{{ channelsLabel }}
</h1>
<KFixedGrid numCols="4">
<KFixedGridItem span="3">
<h1 class="channels-label">
{{ channelsLabel }}
</h1>
</KFixedGridItem>
<KFixedGridItem span="1">
<KSelect
v-model="currentChannelLanguage"
data-test="channel-language-select"
:label="'Show channels for language:'"
:options="languageFilterOptions"
clearable
/>
</KFixedGridItem>
</KFixedGrid>
<p
v-if="isLocalLibraryEmpty"
data-test="nothing-in-lib-label"
Expand Down Expand Up @@ -175,12 +188,16 @@

<script>
import isEmpty from 'lodash/isEmpty';
import sortBy from 'lodash/sortBy';
import uniqBy from 'lodash/uniqBy';
import { get, set } from '@vueuse/core';
import { onMounted, getCurrentInstance, ref, watch } from '@vue/composition-api';
import { computed, onMounted, getCurrentInstance, ref, watch } from '@vue/composition-api';
import commonCoreStrings from 'kolibri/uiText/commonCoreStrings';
import useKResponsiveWindow from 'kolibri-design-system/lib/composables/useKResponsiveWindow';
import useUser from 'kolibri/composables/useUser';
import { getContentLangActive } from 'kolibri/utils/i18n';
import samePageCheckGenerator from 'kolibri-common/utils/samePageCheckGenerator';
import ContentNodeResource from 'kolibri-common/apiResources/ContentNodeResource';
import { mapState } from 'vuex';
Expand Down Expand Up @@ -286,9 +303,11 @@
}
});
const rootNodes = ref([]);
const _rootNodes = ref([]);
const rootNodesLoading = ref(false);
const routeChannelLanguage = computed(() => currentRoute().query.channelLanguage);
function _showChannels(channels, baseurl) {
if (get(isUserLoggedIn) && !baseurl) {
fetchResumableContentNodes();
Expand All @@ -305,7 +324,7 @@
if (shouldResolve()) {
// we want them to be in the same order as the channels list
set(
rootNodes,
_rootNodes,
channels
.map(channel => {
const node = channelCollection.find(n => n.channel_id === channel.id);
Expand All @@ -316,6 +335,7 @@
node.title = channel.name || node.title;
node.thumbnail = channel.thumbnail;
node.description = channel.tagline || channel.description;
node.included_languages = channel.included_languages;
return node;
}
})
Expand All @@ -326,6 +346,22 @@
store.commit('CORE_SET_ERROR', null);
store.commit('SET_PAGE_NAME', PageNames.LIBRARY);
set(rootNodesLoading, false);
if (!get(routeChannelLanguage)) {
// If we have no currently selected channel language, we should try to select one
// based on the user's preferred language.
const closestMatchChannelLanguage = get(channelLanguages)[0];
const channelLanguage =
closestMatchChannelLanguage &&
getContentLangActive(closestMatchChannelLanguage) > 1
? closestMatchChannelLanguage.id
: _allChannelsFlag;
router.replace({
query: {
...currentRoute().query,
channelLanguage,
},
});
}
}
},
error => {
Expand Down Expand Up @@ -387,11 +423,61 @@
watch(() => props.deviceId, showLibrary);
watch(displayingSearchResults, () => {
if (!displayingSearchResults.value && !rootNodes.value.length) {
if (!displayingSearchResults.value && !_rootNodes.value.length) {
showLibrary();
}
});
const channelLanguages = computed(() => {
return sortBy(
uniqBy(
get(_rootNodes).map(node => node.lang),
'id',
),
l => -getContentLangActive(l),
);
});
const languageFilterOptions = computed(() => {
return get(channelLanguages).map(lang => ({
label: lang.lang_name,
value: lang.id,
}));
});
const _allChannelsFlag = 'all';
const currentChannelLanguage = computed({
get() {
if (!get(routeChannelLanguage) || get(routeChannelLanguage) === _allChannelsFlag) {
return {};
}
return get(languageFilterOptions).find(o => o.value === get(routeChannelLanguage)) || {};
},
set(newValue) {
const channelLanguage = newValue?.value || _allChannelsFlag;
if (channelLanguage !== currentRoute().query.channelLanguage) {
router.push({
query: {
...currentRoute().query,
channelLanguage,
},
});
}
},
});
const rootNodes = computed(() => {
if (isEmpty(get(currentChannelLanguage))) {
return get(_rootNodes);
}
return get(_rootNodes).filter(
node =>
node.lang.id === get(currentChannelLanguage).value ||
node.included_languages.includes(get(currentChannelLanguage).value),
);
});
showLibrary();
return {
Expand Down Expand Up @@ -426,6 +512,9 @@
isUserLoggedIn,
canManageContent,
isLearnerOnlyImport,
channelLanguages,
languageFilterOptions,
currentChannelLanguage,
};
},
props: {
Expand Down
22 changes: 20 additions & 2 deletions kolibri/plugins/learn/assets/test/views/library-page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ const CHANNEL = {
name: 'test channel',
root: 'test root',
thumbnail: 'test thumbnail',
lang: {
id: 'es-es',
lang_name: 'Español',
lang_code: 'es',
lang_subcode: 'es',
lang_direction: 'ltr',
},
};

jest.mock('kolibri-common/composables/useChannels');
Expand All @@ -53,7 +60,13 @@ jest.mock('kolibri/urls');

async function makeWrapper({ options, fullMount = false } = {}) {
const store = new Store({
state: { core: { loading: false } },
state: {
core: { loading: false },
route: {
query: {},
name: PageNames.TOPICS_TOPIC,
},
},
getters: {
isUserLoggedIn: jest.fn(),
isLearner: jest.fn(),
Expand All @@ -70,6 +83,9 @@ async function makeWrapper({ options, fullMount = false } = {}) {
SET_PAGE_NAME: jest.fn(),
CORE_SET_PAGE_LOADING: jest.fn(),
CORE_SET_ERROR: jest.fn(),
SET_QUERY(state, query) {
state.route.query = query;
},
},
});
let wrapper;
Expand All @@ -94,8 +110,10 @@ describe('LibraryPage', () => {
}),
);
ContentNodeResource.fetchCollection.mockImplementation(() =>
Promise.resolve([{ id: 'test', title: 'test', channel_id: CHANNEL_ID }]),
Promise.resolve([{ id: 'test', title: 'test', channel_id: CHANNEL_ID, lang: CHANNEL.lang }]),
);
router.push = jest.fn().mockReturnValue(Promise.resolve());
router.replace = jest.fn().mockReturnValue(Promise.resolve());
});
describe('filters button', () => {
it('is visible when the page is not large and channels are available', async () => {
Expand Down
11 changes: 9 additions & 2 deletions packages/kolibri/utils/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ export const getContentLangActive = language => {
const langCode = languageIdToCode(currentLanguage);
const additionalCodes = contentLanguageCodes[langCode] || [];
if (language.id.toLowerCase() === currentLanguage.toLowerCase()) {
// Best possible match, return a 2 to have it still be truthy, but distinguishable
// from a 1 which is a lang_code match
// Best possible match, return a 3 to sort first
// Exact match between content and the language the user is using
return 3;
}
if (language.id === langCode || additionalCodes.includes(language.id)) {
// Here the language for the content has no region code, and we have an exact
// match with the language we are using.
return 2;
}
if (language.lang_code === langCode || additionalCodes.includes(language.lang_code)) {
// Here the language for the content has a region code, but the language itself
// matches, even if the region is different.
return 1;
}
return 0;
Expand Down

0 comments on commit bb22259

Please sign in to comment.