Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
10227a6
Created StudioCollectionsTable
Prashant-thakur77 Nov 3, 2025
5c4b746
Updated ktable to remove sortable propStudioCollectionsTable
Prashant-thakur77 Nov 3, 2025
aff2f08
Remove responsiveness as not needed
Prashant-thakur77 Nov 4, 2025
5a337b2
added proper stles
Prashant-thakur77 Nov 4, 2025
7593d05
Updated Styles
Prashant-thakur77 Nov 4, 2025
db1fc42
Updated Styles
Prashant-thakur77 Nov 4, 2025
5e9cb5e
created StudioCollectionsTable
Prashant-thakur77 Nov 4, 2025
24cd3e0
Updated test file
Prashant-thakur77 Nov 4, 2025
daf9c36
Made few tweaks
Prashant-thakur77 Nov 4, 2025
6d4a9fe
Using ktable empty messgae prop
Prashant-thakur77 Nov 4, 2025
7c8eb95
Made few tweaks
Prashant-thakur77 Nov 4, 2025
8e93066
REplaced Loadingtext with STUdio large loader
Prashant-thakur77 Nov 4, 2025
14dcde2
Used Kmodal own loading feature
Prashant-thakur77 Nov 4, 2025
c468647
Removed exces style
Prashant-thakur77 Nov 4, 2025
0020582
Apllied Kpagecontainer class
Prashant-thakur77 Nov 5, 2025
cba9ba3
Added delete Channel snackbar
Prashant-thakur77 Nov 5, 2025
df764f1
done
Prashant-thakur77 Nov 5, 2025
0631afd
center aligned kpagecontainer elements
Prashant-thakur77 Nov 5, 2025
7ca728a
Updated colum width
Prashant-thakur77 Nov 5, 2025
a8d3848
Updated colum width
Prashant-thakur77 Nov 5, 2025
2f0fdad
updated
Prashant-thakur77 Nov 7, 2025
89e1c81
Center aligned empty text
Prashant-thakur77 Nov 8, 2025
64b8646
Updated tests
Prashant-thakur77 Nov 19, 2025
f2d33fc
Made table header to center align when maxwidth<500px
Prashant-thakur77 Nov 19, 2025
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import VueRouter from 'vue-router';
import ChannelList from './views/Channel/ChannelList';
import ChannelSetList from './views/ChannelSet/ChannelSetList';
import StudioCollectionsTable from './views/ChannelSet/StudioCollectionsTable';
import ChannelSetModal from './views/ChannelSet/ChannelSetModal';
import CatalogList from './views/Channel/CatalogList';
import { RouteNames } from './constants';
Expand All @@ -21,7 +21,7 @@ const router = new VueRouter({
{
name: RouteNames.CHANNEL_SETS,
path: '/collections',
component: ChannelSetList,
component: StudioCollectionsTable,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that ChannelSetList and ChannelSetItem files (+ related obsolete test suites) can be removed since they won't be used anymore from anywhere - can you check on it and delete if that is so?

Before removing them, I recommend you merge latest unstable since we recently did some updates to ChannelSetList (#5543) - that way you hopefully won't have any conflicts during the process.

},
{
name: RouteNames.NEW_CHANNEL_SET,
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are few visual tweaks I'd like us to do, using Kolibri's User table as reference:

review-collection-01

Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
<template>

<KPageContainer class="page-container">
<div class="table-header">
<div class="header-left">
<KButton
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<KButton
  appearance="basic-link"
  :text="$tr('aboutChannelSetsLink')"
  @click="infoDialog = true"
/>
  • Removed unused class
  • Using text prop is slightly more preferred for simple textual values

appearance="basic-link"
class="info-link"
@click="infoDialog = true"
>
{{ $tr('aboutChannelSetsLink') }}
</KButton>

<KModal
v-if="infoDialog"
:title="$tr('aboutChannelSets')"
:cancelText="$tr('cancelButtonLabel')"
@cancel="infoDialog = false"
>
<div class="info-content">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that info-content class and corresponding style can be removed since bottom margin is set globally on paragraphs.

<p>{{ $tr('channelSetsDescriptionText') }}</p>
<p>{{ $tr('channelSetsInstructionsText') }}</p>
<p
class="disclaimer"
:style="{ color: $themeTokens.error }"
>
{{ $tr('channelSetsDisclaimer') }}
</p>
</div>
</KModal>
</div>

<div class="header-right">
<KButton
v-if="!loading"
appearance="raised-button"
primary
data-test="add-channelset"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused attribute

:text="$tr('addChannelSetTitle')"
@click="newChannelSet"
/>
</div>
</div>

<div class="table-container">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused class and div

<KTable
class="collections-table"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused class. There are many other unused classes in this file, I won't comment on all of them. Please check and cleanup all that are not needed.

:stickyColumns="stickyColumns"
caption=""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caption is needed for accessibility. Would you preview https://design-system.learningequality.org/ktable#usage, MDN links, and KDS examples? That will guide you with creating a caption that describes concisely and accurately table content. Also please make it a translated string.

:headers="tableHeaders"
:rows="tableRows"
:dataLoading="loading"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you use useKShow with minVisibleTime set to 500 to display KCircularLoader while data is loading?

See https://design-system.learningequality.org/usekshow for how&why, and you can search the codebase for similar existing implementations.

This means we won't be utilizing dataLoading and emptyMessage props.

(TODO for @MisRob open a KDS issue to apply show and improve loading within the table component itself)

sortable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also use defaultSort (see 'Table with default sort' section https://design-system.learningequality.org/ktable) applied on the first column? This will make the table sorted alphabetically by a collection name which was the original behavior.

disableBuiltinSorting
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be removed. Because of this disable..., sorting doesn't actually function when sort buttons are clicked. I know that the original doesn't have sorting at all, but let's keep it enabled in this refactored version for now.

:emptyMessage="$tr('noChannelSetsFound')"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've recently (#5543) updated link position to be within the empty message:

Would you update this file to achieve similar behavior (but within the new white page container)?

Presence of link in the empty message text will mean that we won't be using emptyMessage prop but implement the empty message logic here in StudioCollectionsTable - this should align nicely together with the other suggestion for loading state improvement.

(TODO for @MisRob: Open KDS KTable issue for emptyMessage slot)

>
<template #cell="{ content, colIndex }">
<!-- Column 0: Collection Name -->
<div
v-if="colIndex === 0"
class="collection-name-cell"
>
<div class="collection-info">
<h3
class="collection-title notranslate"
dir="auto"
>
{{ content }}
</h3>
</div>
</div>

<!-- Column 1: Tokens -->
<div
v-else-if="colIndex === 1"
class="tokens-cell"
>
<StudioCopyToken
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Would you make the token field take the full height of a table row, like here in the current version?

This will optimize how we use space and make rows height smaller.

v-if="content"
:token="content"
class="token-item"
:loading="!content"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loader will never show since StudioCopyToken is mounted only when v-if="content". This line can be removed.

/>
<em
v-else
:style="{ color: $themeTokens.annotation }"
class="saving-text"
>
{{ $tr('saving') }}
</em>
</div>

<!-- Column 2: Channel Count -->
<div
v-else-if="colIndex === 2"
class="channel-count"
>
{{ $formatNumber(content) }}
</div>

<!-- Column 3: Actions -->
<div
v-else-if="colIndex === 3"
class="actions-cell"
>
<KIconButton
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly as requested in the issue for draft version, thank you. I talked about details with designers meanwhile and the final behavior we'd like would be to:

  • Display the full text "Options" button dropdown similarly to the current version:

but only as long as there is enough space on the screen.

  • Change Options button to optionsVertical KIconButton on smaller screens only.

Reasoning being that in contrast to Kolibri's Users table, Collections table has more space and so when we can, we will display the full button to benefit from a bit more clarity.

You will need to use useKResponsiveWindow to switch between the two. I am not quite sure at what breakpoint exactly - we can experiment on some data and see.

icon="optionsVertical"
:aria-label="$tr('options')"
>
<template #menu>
<KDropdownMenu
:options="dropdownOptions"
:hasIcons="true"
@select="option => handleOptionSelect(option, content)"
/>
</template>
</KIconButton>
</div>
</template>
</KTable>
</div>

<!-- Delete Confirmation Modal -->
<KModal
v-if="deleteDialog"
:title="$tr('deleteChannelSetTitle')"
:submitText="$tr('delete')"
:cancelText="$tr('cancel')"
@submit="confirmDelete"
@cancel="deleteDialog = false"
>
<p>{{ $tr('deleteChannelSetText') }}</p>
</KModal>
</KPageContainer>

</template>


<script>
import { mapActions, mapGetters } from 'vuex';
import { RouteNames } from '../../constants';
import StudioCopyToken from '../../../settings/pages/Account/StudioCopyToken';
export default {
name: 'StudioCollectionsTable',
components: {
StudioCopyToken,
},
data() {
return {
loading: true,
infoDialog: false,
deleteDialog: false,
collectionToDelete: null,
};
},
computed: {
...mapGetters('channelSet', ['channelSets', 'getChannelSet']),
tableHeaders() {
return [
{
label: this.$tr('title'),
dataType: 'string',
minWidth: '200px',
width: '55%',
columnId: 'name',
},
{
label: this.$tr('token'),
dataType: 'string',
minWidth: '200px',
width: '20%',
columnId: 'tokens',
},
{
label: this.$tr('channelNumber'),
dataType: 'number',
minWidth: '100px',
width: '15%',
columnId: 'channel_count',
},
{
label: '',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a descriptive label here and then visually hide it? After you have installed learningequality/kolibri-design-system#1169 as suggested in the other comment, visuallyhidden class should work as expected. I think it should be possible to utilize KTable's header slot to apply the class. But this may be the first use-case - let me know if there were any issues.

dataType: 'undefined',
width: '10%',
columnId: 'actions',
},
];
},
tableRows() {
if (!this.channelSets || !Array.isArray(this.channelSets)) {
return [];
}
return this.channelSets.map(collection => {
const channelSet = this.getChannelSet(collection.id);
const channelCount = channelSet?.channels?.length || 0;
return [
collection.name || '',
collection.secret_token || null,
channelCount,
collection.id,
];
});
},
stickyColumns() {
return ['first', 'last'];
},
dropdownOptions() {
return [
{
label: this.$tr('edit'),
value: 'edit',
icon: 'edit',
},
{
label: this.$tr('delete'),
value: 'delete',
icon: 'trash',
},
];
},
},
mounted() {
this.loadChannelSetList().then(() => {
this.loading = false;
});
},
methods: {
...mapActions('channelSet', ['loadChannelSetList', 'deleteChannelSet']),
handleOptionSelect(option, collectionId) {
if (option.value === 'edit') {
this.$router.push({
name: RouteNames.CHANNEL_SET_DETAILS,
params: { channelSetId: collectionId },
});
} else if (option.value === 'delete') {
this.collectionToDelete =
this.getChannelSet(collectionId) ||
this.channelSets.find(c => c.id === collectionId) ||
null;
if (this.collectionToDelete) this.deleteDialog = true;
}
},
confirmDelete() {
if (this.collectionToDelete) {
this.deleteChannelSet(this.collectionToDelete)
.then(() => {
this.loadChannelSetList();
this.deleteDialog = false;
this.collectionToDelete = null;
this.$store.dispatch('showSnackbarSimple', this.$tr('collectionDeleted'));
})
.catch(() => {
this.deleteDialog = false;
this.collectionToDelete = null;
this.$store.dispatch('showSnackbarSimple', this.$tr('deleteError'));
});
}
},
newChannelSet() {
this.$router.push({
name: RouteNames.NEW_CHANNEL_SET,
});
},
},
$trs: {
aboutChannelSetsLink: 'Learn about collections',
aboutChannelSets: 'About collections',
channelSetsDescriptionText:
'A collection contains multiple Kolibri Studio channels that can be imported at one time to Kolibri with a single collection token.',
channelSetsInstructionsText:
'You can make a collection by selecting the channels you want to be imported together.',
channelSetsDisclaimer:
'You will need Kolibri version 0.12.0 or higher to import channel collections',
cancelButtonLabel: 'Close',
addChannelSetTitle: 'New collection',
noChannelSetsFound:
'You can package together multiple channels to create a collection. The entire collection can then be imported to Kolibri at once by using a collection token.',
title: 'Collection name',
token: 'Token ID',
channelNumber: 'Number of channels',
options: 'Options',
deleteChannelSetTitle: 'Delete collection',
deleteChannelSetText: 'Are you sure you want to delete this collection?',
cancel: 'Cancel',
edit: 'Edit collection',
delete: 'Delete collection',
saving: 'Saving',
collectionDeleted: 'Collection deleted',
deleteError: 'Error deleting collection',
},
};
</script>
<style lang="scss" scoped>
.page-container {
width: 100%;
max-width: 1440px;
margin: 0 auto;
text-align: center;
}
.table-header {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.header-left,
.header-right {
display: flex;
align-items: center;
}
.info-content p {
margin-bottom: 16px;
}
.actions-cell {
display: flex;
justify-content: flex-end;
}
@media (max-width: 500px) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We avoid CSS media queries in favor of our layout system - mostly for consistency. Can you use useKResponsiveWindow?

.table-header {
justify-content: center;
}
}
</style>
Loading