Skip to content

Commit 157eb3d

Browse files
authored
Merge pull request #5770 from AlexVelezLl/community_channels_page
Community channels page
2 parents 49cf302 + 21cff99 commit 157eb3d

21 files changed

Lines changed: 1558 additions & 30 deletions

File tree

contentcuration/contentcuration/frontend/channelList/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export const RouteNames = {
2525
CATALOG_ITEMS: 'CATALOG_ITEMS',
2626
CATALOG_DETAILS: 'CATALOG_DETAILS',
2727
CATALOG_FAQ: 'CATALOG_FAQ',
28+
COMMUNITY_LIBRARY_ITEMS: 'COMMUNITY_LIBRARY_ITEMS',
29+
COMMUNITY_LIBRARY_DETAILS: 'COMMUNITY_LIBRARY_DETAILS',
2830
NEW_CHANNEL: 'NEW_CHANNEL',
2931
COMMUNITY_LIBRARY_SUBMISSION: 'COMMUNITY_LIBRARY_SUBMISSION',
3032
};

contentcuration/contentcuration/frontend/channelList/router.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import VueRouter from 'vue-router';
2+
import CommunityChannelDetailsModal from './views/Channel/CommunityLibraryList/CommunityChannelDetailsModal.vue';
23
import StudioMyChannels from './views/Channel/StudioMyChannels';
34
import StudioStarredChannels from './views/Channel/StudioStarredChannels';
45
import StudioViewOnlyChannels from './views/Channel/StudioViewOnlyChannels';
56
import StudioCollectionsTable from './views/ChannelSet/StudioCollectionsTable';
67
import ChannelSetModal from './views/ChannelSet/ChannelSetModal';
78
import CatalogList from './views/Channel/CatalogList';
9+
import CommunityLibraryList from './views/Channel/CommunityLibraryList';
810
import { RouteNames } from './constants';
911
import CatalogFAQ from './views/Channel/CatalogFAQ';
1012
import SubmissionDetailsModal from 'shared/views/communityLibrary/SubmissionDetailsModal/index.vue';
@@ -75,6 +77,17 @@ const router = new VueRouter({
7577
component: ChannelDetailsModal,
7678
props: true,
7779
},
80+
{
81+
name: RouteNames.COMMUNITY_LIBRARY_ITEMS,
82+
path: '/community-library',
83+
component: CommunityLibraryList,
84+
},
85+
{
86+
name: RouteNames.COMMUNITY_LIBRARY_DETAILS,
87+
path: '/community-library/:channelId/details',
88+
component: CommunityChannelDetailsModal,
89+
props: true,
90+
},
7891
{
7992
name: RouteNames.CATALOG_FAQ,
8093
path: '/faq',
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<template>
2+
3+
<KModal
4+
:title="aboutCommunityLibraryTitle$()"
5+
:cancelText="gotItLabel$()"
6+
@cancel="$emit('close')"
7+
>
8+
<p>{{ aboutCommunityLibraryDescription$() }}</p>
9+
<strong>{{ whatCanYouDoHere$() }}</strong>
10+
<ul>
11+
<li>{{ whatCanYouDoHereItem1$() }}</li>
12+
<li>{{ whatCanYouDoHereItem2$() }}</li>
13+
<li>{{ whatCanYouDoHereItem3$() }}</li>
14+
</ul>
15+
<p
16+
:style="{
17+
color: $themeTokens.error,
18+
marginTop: '16px',
19+
}"
20+
>
21+
{{ needKolibriVersionToImport$() }}
22+
</p>
23+
</KModal>
24+
25+
</template>
26+
27+
28+
<script setup>
29+
30+
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';
31+
32+
const {
33+
aboutCommunityLibraryTitle$,
34+
aboutCommunityLibraryDescription$,
35+
whatCanYouDoHere$,
36+
whatCanYouDoHereItem1$,
37+
whatCanYouDoHereItem2$,
38+
whatCanYouDoHereItem3$,
39+
gotItLabel$,
40+
needKolibriVersionToImport$,
41+
} = communityChannelsStrings;
42+
43+
</script>
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<template>
2+
3+
<StudioImmersiveModal v-model="isModalOpen">
4+
<template #header>
5+
<span class="notranslate">{{ channel ? channel.name : '' }}</span>
6+
</template>
7+
<StudioLargeLoader
8+
v-if="show('channelDetails', isLoading, 500)"
9+
:style="{ marginTop: '160px' }"
10+
/>
11+
<div
12+
v-else-if="channel"
13+
:style="{
14+
marginTop: '16px',
15+
}"
16+
>
17+
<StudioDetailsPanel
18+
v-if="channel"
19+
class="channel-details-wrapper"
20+
:details="channel"
21+
:loading="isLoading"
22+
:tokenDefinition="needKolibriVersionToImport$()"
23+
/>
24+
</div>
25+
</StudioImmersiveModal>
26+
27+
</template>
28+
29+
30+
<script>
31+
32+
import Vue, { computed, onMounted, watch } from 'vue';
33+
import useKShow from 'kolibri-design-system/lib/composables/useKShow';
34+
import { useRouter } from 'vue-router/composables';
35+
import StudioDetailsPanel from 'shared/views/details/StudioDetailsPanel.vue';
36+
import StudioLargeLoader from 'shared/views/StudioLargeLoader';
37+
import StudioImmersiveModal from 'shared/views/StudioImmersiveModal';
38+
import useStore from 'shared/composables/useStore';
39+
import { getChannel } from 'shared/data/public';
40+
import { useFetch } from 'shared/composables/useFetch';
41+
import { ChannelVersion } from 'shared/data/resources';
42+
import LanguagesMap from 'shared/leUtils/Languages';
43+
import LicensesMap from 'shared/leUtils/Licenses';
44+
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';
45+
46+
/**
47+
* This function maps the channel info that comes from the public models, to a format that the
48+
* current studio components expect (they are used to receive channel info from the
49+
* contentcuration models)
50+
*/
51+
function mapResponseChannel(channel) {
52+
const language = channel.lang_code || channel.included_languages?.[0] || null;
53+
return {
54+
...channel,
55+
published: true,
56+
count: channel.total_resource_count || 0,
57+
resource_count: channel.total_resource_count || 0,
58+
language,
59+
thumbnail_url: channel.thumbnail,
60+
modified: channel.last_updated,
61+
resource_size: channel.published_size || 0,
62+
last_published: channel.last_published || channel.last_updated,
63+
primary_token: channel.token,
64+
languages:
65+
channel.included_languages?.map(
66+
langCode => LanguagesMap.get(langCode)?.native_name || langCode,
67+
) || [],
68+
licenses:
69+
channel.included_licenses
70+
?.map(licenseId => LicensesMap.get(licenseId)?.license_name)
71+
.filter(Boolean) || [],
72+
73+
// We don't have this data available for public channels,
74+
// so we null it out to avoid confusion
75+
levels: null,
76+
includes: null,
77+
accessible_languages: null,
78+
sample_nodes: null,
79+
tags: null,
80+
authors: null,
81+
copyright_holders: null,
82+
aggregators: null,
83+
providers: null,
84+
};
85+
}
86+
export default {
87+
name: 'ChannelDetailsModal',
88+
components: {
89+
StudioDetailsPanel,
90+
StudioLargeLoader,
91+
StudioImmersiveModal,
92+
},
93+
setup(props) {
94+
const router = useRouter();
95+
const store = useStore();
96+
const isModalOpen = computed({
97+
get: () => true,
98+
set: value => {
99+
if (!value) {
100+
// When the modal is closed, navigate back to the previous page
101+
router.back();
102+
}
103+
},
104+
});
105+
const { show } = useKShow();
106+
107+
const loadChannelDetails = async () => {
108+
try {
109+
const channelResponse = await getChannel(props.channelId, { public: false });
110+
const [channelVersion] = await ChannelVersion.fetchCollection({
111+
channel: channelResponse.id,
112+
version: channelResponse.version,
113+
});
114+
return mapResponseChannel({
115+
...channelResponse,
116+
...channelVersion,
117+
});
118+
} catch (error) {
119+
store.dispatch('errors/handleAxiosError', error);
120+
return null;
121+
}
122+
};
123+
124+
const {
125+
isLoading,
126+
data: channel,
127+
fetchData: fetchChannelDetails,
128+
} = useFetch({
129+
asyncFetchFunc: loadChannelDetails,
130+
});
131+
132+
watch(
133+
() => props.channelId,
134+
channelId => {
135+
if (channelId) {
136+
fetchChannelDetails();
137+
}
138+
},
139+
{ immediate: true },
140+
);
141+
142+
onMounted(() => {
143+
Vue.$analytics.trackAction('community_channel_details', 'View', {
144+
id: props.channelId,
145+
});
146+
});
147+
148+
const { needKolibriVersionToImport$ } = communityChannelsStrings;
149+
150+
return {
151+
show,
152+
isLoading,
153+
channel,
154+
isModalOpen,
155+
156+
needKolibriVersionToImport$,
157+
};
158+
},
159+
props: {
160+
channelId: {
161+
type: String,
162+
default: null,
163+
},
164+
},
165+
};
166+
167+
</script>
168+
169+
170+
<style lang="scss" scoped>
171+
172+
.channel-details-wrapper {
173+
max-width: 900px;
174+
padding-bottom: 100px;
175+
margin: 0 auto;
176+
}
177+
178+
</style>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<template>
2+
3+
<div
4+
class="filters-container"
5+
:class="{ disabled }"
6+
>
7+
<KTextbox
8+
v-model="keywordInput"
9+
clearable
10+
:label="searchLabel$()"
11+
:appearanceOverrides="{ maxWidth: '100%' }"
12+
@input="setKeywords"
13+
/>
14+
15+
<KSelect
16+
v-model="countriesFilter"
17+
:label="countryLabel$()"
18+
:options="countryOptions"
19+
multiple
20+
clearable
21+
/>
22+
23+
<KSelect
24+
v-model="languagesFilter"
25+
:label="languagesLabel$()"
26+
:options="languageOptions"
27+
multiple
28+
clearable
29+
/>
30+
31+
<KSelect
32+
v-model="categoriesFilter"
33+
:label="categoriesLabel$()"
34+
:options="categoryOptions"
35+
multiple
36+
clearable
37+
/>
38+
</div>
39+
40+
</template>
41+
42+
43+
<script setup>
44+
45+
import { injectCommunityChannelsFilters } from './useCommunityChannelsFilters';
46+
import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings';
47+
48+
defineProps({
49+
disabled: {
50+
type: Boolean,
51+
default: false,
52+
},
53+
});
54+
55+
const { searchLabel$, countryLabel$, languagesLabel$, categoriesLabel$ } =
56+
communityChannelsStrings;
57+
58+
const {
59+
keywordInput,
60+
setKeywords,
61+
countriesFilter,
62+
countryOptions,
63+
languagesFilter,
64+
languageOptions,
65+
categoriesFilter,
66+
categoryOptions,
67+
} = injectCommunityChannelsFilters();
68+
69+
</script>
70+
71+
72+
<style lang="scss" scoped>
73+
74+
.filters-container {
75+
display: flex;
76+
flex-direction: column;
77+
gap: 12px;
78+
height: 100%;
79+
min-height: 0;
80+
overflow-y: auto;
81+
82+
&.disabled {
83+
pointer-events: none;
84+
opacity: 0.7;
85+
}
86+
87+
::v-deep .ui-textbox-feedback {
88+
display: none;
89+
}
90+
}
91+
92+
</style>

0 commit comments

Comments
 (0)