diff --git a/contentcuration/contentcuration/frontend/channelEdit/constants.js b/contentcuration/contentcuration/frontend/channelEdit/constants.js index f36ebc5630..f2e5f5bcae 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/constants.js +++ b/contentcuration/contentcuration/frontend/channelEdit/constants.js @@ -74,3 +74,9 @@ export const DraggableRegions = { TOPIC_VIEW: 'topicView', CLIPBOARD: 'clipboard', }; + +/** + * Default page size for the import search page + * @type {number} + */ +export const ImportSearchPageSize = 15; diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ContentTreeList.vue b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ContentTreeList.vue index 3dc29fad36..809bdebd1a 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ContentTreeList.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ContentTreeList.vue @@ -61,6 +61,7 @@ import Checkbox from 'shared/views/form/Checkbox'; import LoadingText from 'shared/views/LoadingText'; import { constantsTranslationMixin } from 'shared/mixins'; + import { ChannelListTypes } from 'shared/constants'; export default { name: 'ContentTreeList', @@ -154,8 +155,14 @@ }, mounted() { this.loading = true; + let params = {}; + const channelListType = this.$route.query.channel_list || ChannelListTypes.PUBLIC; + if (channelListType === ChannelListTypes.PUBLIC) { + params = { published: true }; + } + return Promise.all([ - this.loadChildren({ parent: this.topicId }), + this.loadChildren({ parent: this.topicId, ...params }), this.loadAncestors({ id: this.topicId }), ]).then(() => { this.loading = false; diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchResultsList.vue b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchResultsList.vue index 815f0e49a2..c543cd6228 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchResultsList.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchResultsList.vue @@ -94,10 +94,12 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import debounce from 'lodash/debounce'; import find from 'lodash/find'; + import { ImportSearchPageSize } from '../../constants'; import BrowsingCard from './BrowsingCard'; import SavedSearchesModal from './SavedSearchesModal'; import SearchFilters from './SearchFilters'; import SearchFilterBar from './SearchFilterBar'; + import logging from 'shared/logging'; import Pagination from 'shared/views/Pagination'; import Checkbox from 'shared/views/form/Checkbox'; import LoadingText from 'shared/views/LoadingText'; @@ -137,9 +139,13 @@ nodes() { return this.getContentNodes(this.nodeIds) || []; }, + pageSizeOptions() { + return [10, 15, 25]; + }, pageSize: { get() { - return Number(this.$route.query.page_size) || 25; + const pageSize = Number(this.$route.query.page_size); + return this.pageSizeOptions.find(p => p === pageSize) || ImportSearchPageSize; }, set(page_size) { this.$router.push({ @@ -152,9 +158,6 @@ }); }, }, - pageSizeOptions() { - return [25, 50, 100]; - }, isSelected() { return function(node) { return Boolean(find(this.selected, { id: node.id })); @@ -190,6 +193,7 @@ function() { this.fetchResourceSearchResults({ ...this.$route.query, + page_size: this.pageSize, keywords: this.currentSearchTerm, exclude_channel: this.currentChannelId, last: undefined, @@ -200,8 +204,9 @@ this.pageCount = page.total_pages; this.totalCount = page.count; }) - .catch(() => { + .catch(e => { this.loadFailed = true; + logging.error(e); }); }, 1000, diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js index 89eba97c59..af29d46618 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js @@ -47,8 +47,8 @@ export function loadContentNodeByNodeId(context, nodeId) { }); } -export function loadChildren(context, { parent }) { - return loadContentNodes(context, { parent }); +export function loadChildren(context, { parent, ...params }) { + return loadContentNodes(context, { parent, ...params }); } export function loadAncestors(context, { id }) { diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/importFromChannels/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/importFromChannels/actions.js index 3cb9a1d0b3..4d2be21114 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/importFromChannels/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/importFromChannels/actions.js @@ -1,4 +1,5 @@ import partition from 'lodash/partition'; +import { ImportSearchPageSize } from '../../constants'; import client from 'shared/client'; import urls from 'shared/urls'; import * as publicApi from 'shared/data/public'; @@ -9,7 +10,7 @@ import { Channel, SavedSearch } from 'shared/data/resources'; export async function fetchResourceSearchResults(context, params) { params = { ...params }; delete params['last']; - params.page_size = params.page_size || 25; + params.page_size = params.page_size || ImportSearchPageSize; params.channel_list = params.channel_list || ChannelListTypes.PUBLIC; const response = await client.get(urls.search_list(), { params }); @@ -27,7 +28,7 @@ export async function fetchResourceSearchResults(context, params) { ) : Promise.resolve([]); - await Promise.all([ + const [privateNodesLoaded, publicNodesLoaded] = await Promise.all([ // the loadContentNodes action already loads the nodes into vuex privatePromise, Promise.all( @@ -48,7 +49,26 @@ export async function fetchResourceSearchResults(context, params) { }), ]); - return response.data; + // In case we failed to obtain data for all nodes, filter out the ones we didn't get + const results = response.data.results + .map(node => { + return ( + privateNodesLoaded.find(n => n.id === node.id) || + publicNodesLoaded.find(n => n.id === node.id) + ); + }) + .filter(Boolean); + // This won't work across multiple pages, if we fail to load some nodes, but that should be rare + const countDiff = response.data.results.length - results.length; + const count = response.data.count - countDiff; + const pageDiff = Math.floor(countDiff / params.page_size); + + return { + count, + page: response.data.page, + results, + total_pages: response.data.total_pages - pageDiff, + }; } export function loadChannels(context, params) { diff --git a/contentcuration/contentcuration/frontend/shared/data/public.js b/contentcuration/contentcuration/frontend/shared/data/public.js index 644fb138a8..a6b88b4a5f 100644 --- a/contentcuration/contentcuration/frontend/shared/data/public.js +++ b/contentcuration/contentcuration/frontend/shared/data/public.js @@ -7,6 +7,7 @@ * * See bottom of file for '@typedef's */ +import isString from 'lodash/isString'; import isFunction from 'lodash/isFunction'; import { RolesNames } from 'shared/leUtils/Roles'; import { findLicense } from 'shared/utils/helpers'; @@ -29,6 +30,14 @@ function _convertMetadataLabel(key, obj) { return converted; } +/** + * @see MPTTModel for explanation of this calculation + * @param obj + * @return {number} + */ +const total_count = obj => + obj['kind'] === ContentKindsNames.TOPIC ? (obj['rght'] - obj['lft'] - 1) / 2 : 1; + const CONTENT_NODE_FIELD_MAP = { // `destination field`: `source field` or value producing function node_id: 'id', @@ -54,21 +63,23 @@ const CONTENT_NODE_FIELD_MAP = { categories: _convertMetadataLabel.bind({}, 'categories'), resource_types: _convertMetadataLabel.bind({}, 'resource_types'), tags: _convertMetadataLabel.bind({}, 'tags'), - extra_fields: obj => ({ options: JSON.parse(obj['options']) }), + extra_fields: obj => ({ + options: isString(obj['options']) ? JSON.parse(obj['options']) : obj['options'], + }), role_visibility: obj => (obj['coach_content'] ? RolesNames.COACH : RolesNames.LEARNER), license: obj => findLicense(obj['license_name']), license_description: 'license_description', copyright_holder: 'license_owner', coach_count: 'num_coach_contents', - // see MPTTModel for explanation of this calculation - total_count: obj => - obj['kind'] === ContentKindsNames.TOPIC ? (obj['rght'] - obj['lft'] - 1) / 2 : 1, + + total_count, language: obj => obj['lang']['id'], - thumbnail_src: obj => new URL(obj['thumbnail'], window.location.origin).pathname, + thumbnail_src: obj => + obj['thumbnail'] ? new URL(obj['thumbnail'], window.location.origin).toString() : null, thumbnail_encoding: () => '{}', // unavailable in public API - // 'resource_count': 'num_resources', + resource_count: total_count, // fake it, assume all nodes are resources // 'root_id': 'root_id', };