-
Notifications
You must be signed in to change notification settings - Fork 287
Channel cards to unstable #5696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from all commits
0be0e93
ad0d85f
b7c3b45
2213979
d3f3a17
fbfe7c6
07c0e13
04f1ab8
d69002a
ec17f60
32093d9
217c912
6840bdd
c277e85
eefe23f
5327a26
36f359e
b0cfdcb
e52d071
a79502a
3aded04
c837c95
512e2f1
4d66ec2
841257a
7ee25b9
bc092cd
aa8b0a9
cc563d2
b0479ef
a820fb4
18f4f26
82f701b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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} Loading state and filtered & sorted channels | ||
| */ | ||
| 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, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,9 +26,7 @@ | |
| fluid | ||
| :style="`margin-top: ${offline ? 48 : 0}`" | ||
| > | ||
| <LoadingText v-if="loading" /> | ||
| <VLayout | ||
| v-else | ||
| grid | ||
| wrap | ||
| class="list-wrapper" | ||
|
|
@@ -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" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder whether we should continue to use Vuetify's styling classes especially when we want to move away from them (I see other places where its used as well). This is more of a thought than a comment and something to think about long term? |
||
| :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 | ||
|
|
@@ -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'; | ||
|
|
@@ -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() { | ||
|
|
@@ -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 | ||
|
|
@@ -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; | ||
|
|
@@ -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. | ||
|
|
@@ -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) { | ||
|
|
@@ -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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } 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 = { | ||
|
|
@@ -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', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this fails to load (like it times out), what happens from the user perspective?