Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0be0e93
[Remove Vuetify from Studio] Cards in My Channels
yeshwanth235 Nov 7, 2025
ad0d85f
[Remove Vuetify from Studio] Cards in Starred channels changes
yeshwanth235 Dec 8, 2025
b7c3b45
[Remove Vuetify from Studio] Cards in View-only channels changes
yeshwanth235 Dec 17, 2025
2213979
Merge branch 'unstable' into channel-cards
MisRob Jan 21, 2026
d3f3a17
Merge pull request #5657 from MisRob/channel-cards
MisRob Jan 21, 2026
fbfe7c6
Merge branch 'unstable' into channel-cards-finalize
MisRob Feb 9, 2026
07c0e13
Use 16:9 ratio for thumbnails
MisRob Feb 9, 2026
04f1ab8
Remove unnecessary style definition
MisRob Feb 10, 2026
d69002a
Fix token reference
MisRob Feb 10, 2026
ec17f60
Do not add KTooltip to DOM when not necessary
MisRob Feb 10, 2026
32093d9
Abstract page layout into a component
MisRob Feb 10, 2026
217c912
Move invitations box to StudioMyChannels
MisRob Feb 10, 2026
6840bdd
[Remove Vuetify from Studio] Cards in Content Library
vtushar06 Feb 10, 2026
c277e85
Merge branch 'channel-cards-finalize' into channel-cards
MisRob Feb 10, 2026
eefe23f
Remove handler call
MisRob Feb 10, 2026
5327a26
Configure skeleton loaders to reflect
MisRob Feb 10, 2026
36f359e
Add bottom padding
MisRob Feb 10, 2026
b0cfdcb
Add local claude settings to gitignore
MisRob Feb 10, 2026
e52d071
Align test suite with other suites regarding channel cards
MisRob Feb 10, 2026
a79502a
Rename method
MisRob Feb 10, 2026
3aded04
Fix semantics
MisRob Feb 10, 2026
c837c95
Show view-only invitations
MisRob Feb 10, 2026
512e2f1
Fix card click targets
MisRob Feb 10, 2026
4d66ec2
Footer configuration
MisRob Feb 12, 2026
841257a
Optimize card orientation in catalog
MisRob Feb 10, 2026
7ee25b9
Optimize card loading
MisRob Feb 14, 2026
86d267e
[TODO REVERT] Temporarily install KDS fork
MisRob Feb 14, 2026
de1d719
Disable syncCardMetrics to prevent unnecessary
MisRob Feb 14, 2026
8fc6382
Fix typo
MisRob Feb 14, 2026
c94c96a
Remove unused code
MisRob Feb 14, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var/

# Ignore editor / IDE related data
.vscode/
.claude/

# IntelliJ IDE, except project config
.idea/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ref, computed, onMounted, getCurrentInstance } from 'vue';
import orderBy from 'lodash/orderBy';

/**
* Composable for channel list functionality
*
* @param {Object} options - Configuration options
* @param {string} options.listType - Type of channel list (from ChannelListTypes)
* @param {Array<string>} options.sortFields - Fields to sort by (default: ['modified'])
* @param {Array<string>} options.orderFields - Sort order (default: ['desc'])
* @returns {Object} Channel list state and methods
*/
export function useChannelList(options = {}) {
const { listType, sortFields = ['modified'], orderFields = ['desc'] } = options;

const instance = getCurrentInstance();
const store = instance.proxy.$store;

const loading = ref(false);

const allChannels = computed(() => store.getters['channel/channels'] || []);

const channels = computed(() => {
if (!allChannels.value || allChannels.value.length === 0) {
return [];
}

const filtered = allChannels.value.filter(channel => channel[listType] && !channel.deleted);

return orderBy(filtered, sortFields, orderFields);
});

onMounted(() => {
loading.value = true;
store.dispatch('channel/loadChannelList', { listType }).then(() => {
loading.value = false;
});
});

return {
loading,
channels,
};
}
15 changes: 6 additions & 9 deletions contentcuration/contentcuration/frontend/channelList/router.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import VueRouter from 'vue-router';
import ChannelList from './views/Channel/ChannelList';
import StudioMyChannels from './views/Channel/StudioMyChannels';
import StudioStarredChannels from './views/Channel/StudioStarredChannels';
import StudioViewOnlyChannels from './views/Channel/StudioViewOnlyChannels';
import StudioCollectionsTable from './views/ChannelSet/StudioCollectionsTable';
import ChannelSetModal from './views/ChannelSet/ChannelSetModal';
import CatalogList from './views/Channel/CatalogList';
import { RouteNames } from './constants';
import CatalogFAQ from './views/Channel/CatalogFAQ';
import ChannelModal from 'shared/views/channel/ChannelModal';
import ChannelDetailsModal from 'shared/views/channel/ChannelDetailsModal';
import { ChannelListTypes } from 'shared/constants';

const router = new VueRouter({
routes: [
{
name: RouteNames.CHANNELS_EDITABLE,
path: '/my-channels',
component: ChannelList,
props: { listType: ChannelListTypes.EDITABLE },
component: StudioMyChannels,
},

{
name: RouteNames.CHANNEL_SETS,
path: '/collections',
Expand All @@ -38,14 +37,12 @@ const router = new VueRouter({
{
name: RouteNames.CHANNELS_STARRED,
path: '/starred',
component: ChannelList,
props: { listType: ChannelListTypes.STARRED },
component: StudioStarredChannels,
},
{
name: RouteNames.CHANNELS_VIEW_ONLY,
path: '/view-only',
component: ChannelList,
props: { listType: ChannelListTypes.VIEW_ONLY },
component: StudioViewOnlyChannels,
},
{
name: RouteNames.CHANNEL_DETAILS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@
fluid
:style="`margin-top: ${offline ? 48 : 0}`"
>
<LoadingText v-if="loading" />
<VLayout
v-else
grid
wrap
class="list-wrapper"
Expand All @@ -37,42 +35,53 @@
xs12
class="mb-2"
>
<h1 class="mb-2 ml-1 title">
{{ $tr('resultsText', { count: page.count }) }}
</h1>
<KButton
v-if="page.count && !selecting"
:text="$tr('selectChannels')"
data-testid="select"
appearance="basic-link"
@click="setSelection(true)"
/>
<Checkbox
v-else-if="selecting"
<h1 class="visuallyhidden">{{ $tr('title') }}</h1>
<!-- minHeight to prevent layout shifts when loading state changes -->
<p
class="mb-2 ml-1 title"
:style="{ minHeight: '30px' }"
>
<span v-if="!loading">{{ $tr('resultsText', { count: page.count }) }}</span>
</p>
<!-- minHeight to prevent layout shifts when loading state changes -->
<div :style="{ minHeight: '30px' }">
<KButton
v-if="page.count && !selecting && !loading"
:text="$tr('selectChannels')"
appearance="basic-link"
@click="setSelection(true)"
/>
</div>
<KCheckbox
v-if="selecting"
v-model="selectAll"
class="mb-4 mx-2"
:indeterminate="isIndeterminate"
:label="$tr('selectAll')"
:indeterminate="selected.length > 0 && selected.length < channels.length"
class="mb-4 mx-2"
/>
</VFlex>
<VFlex xs12>
<VLayout
v-for="item in channels"
:key="item.id"
align-center
<KCardGrid
layout="1-1-1"
:loading="loading"
:skeletonsConfig="skeletonsConfig"
:syncCardsMetrics="false"
>
<Checkbox
v-show="selecting"
v-model="selected"
class="mx-2"
:value="item.id"
/>
<ChannelItem
:channelId="item.id"
:detailsRouteName="detailsRouteName"
style="flex-grow: 1; width: 100%"
<StudioChannelCard
v-for="channel in channels"
:key="channel.id"
:headingLevel="2"
:orientation="windowBreakpoint > 2 ? 'horizontal' : 'vertical'"
:showUpdateStatus="false"
:channel="channel"
:footerButtons="getFooterButtons(channel)"
:dropdownOptions="getDropdownOptions(channel)"
:selectable="selecting"
:selected="isChannelSelected(channel)"
@toggle-selection="handleSelectionToggle"
@click="onCardClick(channel)"
/>
</VLayout>
</KCardGrid>
</VFlex>
<VFlex
xs12
Expand Down Expand Up @@ -136,11 +145,9 @@
import { RouteNames } from '../../constants';
import CatalogFilters from './CatalogFilters';
import CatalogFilterBar from './CatalogFilterBar';
import ChannelItem from './ChannelItem';
import LoadingText from 'shared/views/LoadingText';
import StudioChannelCard from './StudioChannelCard';
import Pagination from 'shared/views/Pagination';
import BottomBar from 'shared/views/BottomBar';
import Checkbox from 'shared/views/form/Checkbox';
import ToolBar from 'shared/views/ToolBar';
import OfflineText from 'shared/views/OfflineText';
import { constantsTranslationMixin } from 'shared/mixins';
Expand All @@ -149,22 +156,21 @@
export default {
name: 'CatalogList',
components: {
ChannelItem,
LoadingText,
StudioChannelCard,
CatalogFilters,
CatalogFilterBar,
Pagination,
BottomBar,
Checkbox,
ToolBar,
OfflineText,
},
mixins: [channelExportMixin, constantsTranslationMixin],
setup() {
const { windowIsSmall } = useKResponsiveWindow();
const { windowIsSmall, windowBreakpoint } = useKResponsiveWindow();

return {
windowIsSmall,
windowBreakpoint,
};
},
data() {
Expand All @@ -178,8 +184,12 @@
* differences between previous query params and new
* query params, so just track it manually
*/
previousQuery: this.$route.query,

/**
* MisRob: Add 'page: 1' as default to prevent it from being
* added later and causing redundant $router watcher call when
* page initially loading (fixes loading state showing twice)
*/
previousQuery: { page: 1, ...this.$route.query },
/**
* jayoshih: using excluded logic here instead of selected
* to account for selections across pages (some channels
Expand All @@ -190,10 +200,29 @@
},
computed: {
...mapGetters('channel', ['getChannels']),
...mapGetters(['loggedIn']),
...mapState('channelList', ['page']),
...mapState({
offline: state => !state.connection.online,
}),
skeletonsConfig() {
return [
{
breakpoints: [0, 1, 2, 3, 4, 5, 6, 7],
count: 2,
orientation: 'vertical',
thumbnailDisplay: 'small',
thumbnailAlign: 'left',
thumbnailAspectRatio: '16:9',
minHeight: '380px',
},
{
breakpoints: [3, 4, 5, 6, 7],
orientation: 'horizontal',
minHeight: '230px',
},
];
},
selectAll: {
get() {
return this.selected.length === this.channels.length;
Expand All @@ -216,9 +245,6 @@
debouncedSearch() {
return debounce(this.loadCatalog, 1000);
},
detailsRouteName() {
return RouteNames.CATALOG_DETAILS;
},
channels() {
// Sort again by the same ordering used on the backend - name.
// Have to do this because of how we are getting the object data via getChannels.
Expand All @@ -227,6 +253,9 @@
selectedCount() {
return this.page.count - this.excluded.length;
},
isIndeterminate() {
return this.selected.length > 0 && this.selected.length < this.channels.length;
},
},
watch: {
$route(to) {
Expand All @@ -245,11 +274,58 @@
this.previousQuery = { ...to.query };
},
},
mounted() {
created() {
this.loadCatalog();
},
methods: {
...mapActions('channelList', ['searchCatalog']),
getFooterButtons(channel) {
const buttons = ['info'];
if (channel.published) {
buttons.push('copy');
}
if (this.loggedIn) {
buttons.push('bookmark');
}
return buttons;
},
getDropdownOptions(channel) {
const options = [];
if (channel.source_url) {
options.push('source-url');
}
if (channel.demo_server_url) {
options.push('demo-url');
}
return options;
},
onCardClick(channel) {
if (this.loggedIn) {
window.location.href = window.Urls.channel(channel.id);
} else {
this.$router.push({
name: RouteNames.CHANNEL_DETAILS,
query: {
...this.$route.query,
last: this.$route.name,
},
params: {
channelId: channel.id,
},
});
}
},
isChannelSelected(channel) {
return this.selected.includes(channel.id);
},
handleSelectionToggle(channelId) {
const currentlySelected = this.selected;
if (currentlySelected.includes(channelId)) {
this.selected = currentlySelected.filter(id => id !== channelId);
} else {
this.selected = [...currentlySelected, channelId];
}
},
loadCatalog() {
this.loading = true;
const params = {
Expand Down Expand Up @@ -297,6 +373,7 @@
},
},
$trs: {
title: 'Content library',
resultsText: '{count, plural,\n =1 {# result found}\n other {# results found}}',
selectChannels: 'Download a summary of selected channels',
cancelButton: 'Cancel',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,6 @@

<style lang="scss" scoped>
/deep/ .v-list__tile {
height: unset;
padding: 16px;
cursor: default;
}
.v-list__tile__title {
height: unset;
white-space: unset;
}
.invitation {
padding: 16px 16px 0;
font-size: 16px;
Expand Down
Loading