From 00b2506a7ab61993bafd11b41c3632727ab55ed9 Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Fri, 20 Feb 2026 08:19:08 -0500 Subject: [PATCH 01/13] Add page setup and channel info header --- .../frontend/channelList/constants.js | 1 + .../frontend/channelList/router.js | 7 + .../frontend/shared/data/resources.js | 4 + .../frontend/shared/strings/commonStrings.js | 4 + .../strings/communityChannelsStrings.js | 21 ++ .../frontend/shared/utils/helpers.js | 15 + .../NotificationsModal/NotificationList.vue | 9 + .../CommunityLibrarySubmissionApproval.vue | 1 + .../CommunityLibrarySubmissionCreation.vue | 1 + .../CommunityLibrarySubmissionRejection.vue | 1 + .../CommunityLibraryStatusChip.vue | 19 +- .../SubmissionDetailsModal/index.vue | 302 ++++++++++++++++++ .../viewsets/community_library_submission.py | 19 +- 13 files changed, 402 insertions(+), 2 deletions(-) create mode 100644 contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue diff --git a/contentcuration/contentcuration/frontend/channelList/constants.js b/contentcuration/contentcuration/frontend/channelList/constants.js index e2caafa367..176bd4fc0e 100644 --- a/contentcuration/contentcuration/frontend/channelList/constants.js +++ b/contentcuration/contentcuration/frontend/channelList/constants.js @@ -26,6 +26,7 @@ export const RouteNames = { CATALOG_DETAILS: 'CATALOG_DETAILS', CATALOG_FAQ: 'CATALOG_FAQ', NEW_CHANNEL: 'NEW_CHANNEL', + COMMUNITY_LIBRARY_SUBMISSION: 'COMMUNITY_LIBRARY_SUBMISSION', }; export const ListTypeToRouteMapping = { diff --git a/contentcuration/contentcuration/frontend/channelList/router.js b/contentcuration/contentcuration/frontend/channelList/router.js index 46c50e94e2..e6bda8aed2 100644 --- a/contentcuration/contentcuration/frontend/channelList/router.js +++ b/contentcuration/contentcuration/frontend/channelList/router.js @@ -7,6 +7,7 @@ import ChannelSetModal from './views/ChannelSet/ChannelSetModal'; import CatalogList from './views/Channel/CatalogList'; import { RouteNames } from './constants'; import CatalogFAQ from './views/Channel/CatalogFAQ'; +import SubmissionDetailsModal from 'shared/views/communityLibrary/SubmissionDetailsModal/index.vue'; import ChannelModal from 'shared/views/channel/ChannelModal'; import ChannelDetailsModal from 'shared/views/channel/ChannelDetailsModal'; @@ -79,6 +80,12 @@ const router = new VueRouter({ path: '/faq', component: CatalogFAQ, }, + { + name: RouteNames.COMMUNITY_LIBRARY_SUBMISSION, + path: '/community-library/:channelId/:submissionId', + component: SubmissionDetailsModal, + props: true, + }, // Catch-all for unrecognized URLs { path: '*', diff --git a/contentcuration/contentcuration/frontend/shared/data/resources.js b/contentcuration/contentcuration/frontend/shared/data/resources.js index 652572a3bc..2a458cd2a3 100644 --- a/contentcuration/contentcuration/frontend/shared/data/resources.js +++ b/contentcuration/contentcuration/frontend/shared/data/resources.js @@ -2415,6 +2415,10 @@ export const CommunityLibrarySubmission = new APIResource({ return response.data || []; }); }, + async fetchModel(id) { + const response = await client.get(this.modelUrl(id)); + return response.data; + }, create(params) { return client.post(this.collectionUrl(), params).then(response => { return response.data; diff --git a/contentcuration/contentcuration/frontend/shared/strings/commonStrings.js b/contentcuration/contentcuration/frontend/shared/strings/commonStrings.js index 14f9d7518a..5ca0b7a0f9 100644 --- a/contentcuration/contentcuration/frontend/shared/strings/commonStrings.js +++ b/contentcuration/contentcuration/frontend/shared/strings/commonStrings.js @@ -10,6 +10,10 @@ export const commonStrings = createTranslator('CommonStrings', { message: 'Clear', context: 'A label for an action that clears a selection or input field', }, + seeAllAction: { + message: 'See All', + context: 'A label for an action that shows all items in a list or collection', + }, closeAction: { message: 'Close', context: 'A label for an action that closes a dialog or window', diff --git a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js index 8c1c9186bc..37eeed8fac 100644 --- a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js +++ b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js @@ -118,6 +118,11 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin message: 'Submitted', context: 'Status indicating that an Community Library submission is pending', }, + superseededStatus: { + message: 'Superseded', + context: + 'Status indicating that an Community Library submission is superseded by a newer submission', + }, approvedStatus: { message: 'Approved', context: 'Status indicating that an Community Library submission is approved', @@ -126,6 +131,10 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin message: 'Flagged', context: 'Status indicating that an Community Library submission is rejected', }, + liveStatus: { + message: 'Live', + context: 'Status indicating that an Community Library submission is live', + }, // Submit to Community Library panel strings submitToCommunityLibrary: { message: 'Submit to Community Library', @@ -414,4 +423,16 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin context: 'Notice for screen readers on the new notifications badge to indicate that new notifications have arrived', }, + communityLibrarySubmissionLabel: { + message: 'Community Library submission', + context: 'Label for notifications related to Community Library submissions', + }, + channelVersionTokenLabel: { + message: 'Channel version token', + context: 'Label for the channel version token included in submission details page', + }, + liveVersionLabel: { + message: 'Live version:', + context: 'Label indicating the live version of a channel', + }, }); diff --git a/contentcuration/contentcuration/frontend/shared/utils/helpers.js b/contentcuration/contentcuration/frontend/shared/utils/helpers.js index f5fa5a16d8..7da7eeab02 100644 --- a/contentcuration/contentcuration/frontend/shared/utils/helpers.js +++ b/contentcuration/contentcuration/frontend/shared/utils/helpers.js @@ -656,3 +656,18 @@ export function getMergedMapFields(node, contentNodeData) { } return mergedMapFields; } + +export function getCommunityLibrarySubmissionDetailsUrl(channelId, submissionId) { + const pathname = window.location.pathname; + const hash = window.location.hash; + const prevUrl = pathname + hash; + + const externalUrl = `/channels/#/community-library/${channelId}/${submissionId}`; + + const queryParams = new URLSearchParams({ + back: prevUrl, + }); + const query = queryParams.toString(); + + return `${externalUrl}?${query}`; +} diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/NotificationList.vue b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/NotificationList.vue index 9323c3210b..618a575147 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/NotificationList.vue +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/NotificationList.vue @@ -29,6 +29,7 @@ v-for="notification in notifications" :key="`${notification.id}-${notification.type}`" :notification="notification" + @viewMore="goToSubmissionDetails(notification)" />
{ + const channelId = notification.channel_id; + const submissionId = notification.id; + const url = getCommunityLibrarySubmissionDetailsUrl(channelId, submissionId); + window.location.href = url; + }; + const { newLabel$, clearAllAction$, diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionApproval.vue b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionApproval.vue index 845560f67d..ef44fdc57d 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionApproval.vue +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionApproval.vue @@ -23,6 +23,7 @@ diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionCreation.vue b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionCreation.vue index 88499564b9..2b1d82488d 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionCreation.vue +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionCreation.vue @@ -17,6 +17,7 @@ diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionRejection.vue b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionRejection.vue index 8e58fc6f64..c61c71bf1e 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionRejection.vue +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/notificationTypes/CommunityLibrarySubmissionRejection.vue @@ -23,6 +23,7 @@ diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/CommunityLibraryStatusChip.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/CommunityLibraryStatusChip.vue index e04ec58d2a..07d42fb130 100644 --- a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/CommunityLibraryStatusChip.vue +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/CommunityLibraryStatusChip.vue @@ -26,8 +26,10 @@ required: true, validator: value => [ + CommunityLibraryStatus.LIVE, CommunityLibraryStatus.APPROVED, CommunityLibraryStatus.PENDING, + CommunityLibraryStatus.SUPERSEDED, CommunityLibraryStatus.REJECTED, ].includes(value), }, @@ -35,9 +37,17 @@ const theme = themePalette(); - const { pendingStatus$, approvedStatus$, flaggedStatus$ } = communityChannelsStrings; + const { pendingStatus$, superseededStatus$, approvedStatus$, flaggedStatus$, liveStatus$ } = + communityChannelsStrings; const configChoices = { + [CommunityLibraryStatus.SUPERSEDED]: { + text: superseededStatus$(), + backgroundColor: theme.yellow.v_100, + labelColor: theme.orange.v_600, + borderColor: theme.orange.v_400, + icon: 'timer', + }, [CommunityLibraryStatus.PENDING]: { text: pendingStatus$(), backgroundColor: theme.yellow.v_100, @@ -52,6 +62,13 @@ borderColor: theme.green.v_400, icon: 'circleCheckmark', }, + [CommunityLibraryStatus.LIVE]: { + text: liveStatus$(), + backgroundColor: theme.green.v_100, + labelColor: theme.green.v_600, + borderColor: theme.green.v_400, + icon: 'circleCheckmark', + }, [CommunityLibraryStatus.REJECTED]: { text: flaggedStatus$(), backgroundColor: theme.red.v_100, diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue new file mode 100644 index 0000000000..48f746c33a --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue @@ -0,0 +1,302 @@ + + + + + + + diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py index aef0d87f0a..2e91e08bf5 100644 --- a/contentcuration/contentcuration/viewsets/community_library_submission.py +++ b/contentcuration/contentcuration/viewsets/community_library_submission.py @@ -1,3 +1,5 @@ +from django.db.models import OuterRef +from django.db.models import Subquery from django_filters import BaseInFilter from django_filters import ChoiceFilter from django_filters.rest_framework import DateTimeFilter @@ -15,6 +17,7 @@ ) from contentcuration.models import Change from contentcuration.models import Channel +from contentcuration.models import ChannelVersion from contentcuration.models import CommunityLibrarySubmission from contentcuration.models import Country from contentcuration.tasks import apply_channel_changes_task @@ -278,9 +281,23 @@ class AdminCommunityLibrarySubmissionViewSet( ): permission_classes = [IsAdminUser] - values = CommunityLibrarySubmissionViewSetMixin.values + ("internal_notes",) + values = CommunityLibrarySubmissionViewSetMixin.values + ( + "internal_notes", + "version_token", + ) field_map = CommunityLibrarySubmissionViewSetMixin.field_map.copy() + def annotate_queryset(self, queryset): + queryset = super().annotate_queryset(queryset) + return queryset.annotate( + version_token=Subquery( + ChannelVersion.objects.filter( + channel_id=OuterRef("channel_id"), + version=OuterRef("channel_version"), + ).values("secret_token__token")[:1] + ) + ) + def _mark_previous_pending_submissions_as_superseded(self, submission): CommunityLibrarySubmission.objects.filter( status=community_library_submission_constants.STATUS_PENDING, From 082ed1b6862072e3ec2bd8afc3bd1e1b7b43fd6e Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Fri, 20 Feb 2026 13:13:17 -0500 Subject: [PATCH 02/13] Add Channel details accordion --- .../composables/useSpecialPermissions.js | 11 +- .../frontend/shared/strings/commonStrings.js | 10 +- .../strings/communityChannelsStrings.js | 38 ++- .../SpecialPermissionsList.vue | 30 +- .../SubmissionDetailsModal/Accordion.vue | 70 +++++ .../SubmissionDetailsModal/ChannelDetails.vue | 279 ++++++++++++++++++ .../SubmissionDetailsModal/index.vue | 29 +- .../views/details/StudioDetailsPanel.vue | 182 +++++++----- .../contentcuration/viewsets/channel.py | 3 + 9 files changed, 560 insertions(+), 92 deletions(-) create mode 100644 contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/Accordion.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ChannelDetails.vue diff --git a/contentcuration/contentcuration/frontend/shared/composables/useSpecialPermissions.js b/contentcuration/contentcuration/frontend/shared/composables/useSpecialPermissions.js index 306ce4d83c..1224cbc8f2 100644 --- a/contentcuration/contentcuration/frontend/shared/composables/useSpecialPermissions.js +++ b/contentcuration/contentcuration/frontend/shared/composables/useSpecialPermissions.js @@ -23,7 +23,7 @@ const ITEMS_PER_PAGE = 3; * Reactive state for the fetched, flattened permissions and pagination * helpers used by `SpecialPermissionsList.vue`. */ -export function useSpecialPermissions(channelVersionId) { +export function useSpecialPermissions(channelVersionId, { distributable }) { const permissions = ref([]); const isLoading = ref(false); const error = ref(null); @@ -46,10 +46,13 @@ export function useSpecialPermissions(channelVersionId) { try { if (versionId) { - const response = await AuditedSpecialPermissionsLicense.fetchCollection({ + const filters = { channel_version: versionId, - distributable: false, - }); + }; + if (distributable !== null) { + filters.distributable = distributable; + } + const response = await AuditedSpecialPermissionsLicense.fetchCollection(filters); permissions.value = response; } diff --git a/contentcuration/contentcuration/frontend/shared/strings/commonStrings.js b/contentcuration/contentcuration/frontend/shared/strings/commonStrings.js index 5ca0b7a0f9..299ddd3d1e 100644 --- a/contentcuration/contentcuration/frontend/shared/strings/commonStrings.js +++ b/contentcuration/contentcuration/frontend/shared/strings/commonStrings.js @@ -11,9 +11,13 @@ export const commonStrings = createTranslator('CommonStrings', { context: 'A label for an action that clears a selection or input field', }, seeAllAction: { - message: 'See All', + message: 'See all', context: 'A label for an action that shows all items in a list or collection', }, + seeLessAction: { + message: 'See less', + context: 'A label for an action that shows fewer items in a list or collection', + }, closeAction: { message: 'Close', context: 'A label for an action that closes a dialog or window', @@ -22,4 +26,8 @@ export const commonStrings = createTranslator('CommonStrings', { message: 'Sorry! Something went wrong, please try again.', context: 'Default error message for operation errors.', }, + channelDetailsLabel: { + message: 'Channel Details', + context: 'Label for a section that displays details about a channel', + }, }); diff --git a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js index 37eeed8fac..ddfb5ac4fd 100644 --- a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js +++ b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js @@ -244,21 +244,35 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin 'Snackbar message shown when submission fails from the "Submit to Community Library" panel', }, countryLabel: { - message: 'Country', + message: 'Country(s)', context: 'Label for the country selection field in the "Submit to Community Library" panel', }, - languagesDetected: { - message: 'Language(s) detected', + languagesLabel: { + message: 'Language(s)', context: 'Label for detected languages in the "Submit to Community Library" panel', }, - licensesDetected: { - message: 'License(s) detected', + licensesLabel: { + message: 'License(s)', context: 'Label for detected licenses in the "Submit to Community Library" panel', }, - categoriesDetected: { + categoriesLabel: { message: 'Categories', context: 'Label for detected categories in the "Submit to Community Library" panel', }, + submissionNotesLabel: { + message: 'Submission notes', + context: 'Label for the notes the editor can add to their submission to the Community Library', + }, + feedbackNotesLabel: { + message: 'Feedback notes', + context: + 'Label for the feedback notes that reviewers can add to a submission in the Community Library ', + }, + internalNotesLabel: { + message: 'Internal notes', + context: + 'Label for the notes that admins can add to a submission in the Community Library for themselves', + }, confirmReplacementText: { message: 'I understand this will replace my earlier submission on the review queue', context: 'Checkbox text shown when there is a pending submission to confirm replacement', @@ -402,6 +416,14 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin message: '{author} ({userType}) flagged {channelVersion}', context: 'Notification message shown when a user flags a channel version for review', }, + submissionNotification: { + message: '{author} ({userType}) submitted {channelVersion}', + context: 'Notification message shown when a user submits a channel version for review', + }, + approvedNotification: { + message: '{author} ({userType}) approved {channelVersion}', + context: 'Notification message shown when a user approves a channel version', + }, showOlderAction: { message: 'Show older', context: 'Action button to load older items in a list', @@ -410,6 +432,10 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin message: 'Admin', context: 'Label indicating administrative status', }, + editorLabel: { + message: 'Editor', + context: 'Label indicating editor status', + }, emptyNotificationsNotice: { message: 'You have no notifications at this time.', context: 'Notice shown when there are no notifications to display', diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SpecialPermissionsList.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SpecialPermissionsList.vue index 5a449d1015..eb4f59d9c0 100644 --- a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SpecialPermissionsList.vue +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SpecialPermissionsList.vue @@ -13,7 +13,10 @@
{{ specialPermissionsDetected$() }}
-
+
{{ confirmDistributionRights$() }} @@ -23,9 +26,10 @@ @@ -89,9 +93,21 @@ totalPages, nextPage, previousPage, - } = useSpecialPermissions(props.channelVersionId); + } = useSpecialPermissions(props.channelVersionId, { + // only load non-distributable permissions for editable special permission lists + // as those are the ones that the user should check + distributable: props.readOnly ? null : false, + }); + + function isPermissionChecked(permission) { + if (props.readOnly) { + return permission.distributable; + } + return props.value.includes(permission.id); + } function togglePermission(permissionId) { + if (props.disabled || props.readOnly) return; const currentChecked = [...props.value]; const index = currentChecked.indexOf(permissionId); if (index === -1) { @@ -104,7 +120,7 @@ const allChecked = computed(() => { if (isLoading.value) return false; - return permissions.value.every(p => props.value.includes(p.id)); + return permissions.value.every(p => isPermissionChecked(p)); }); watch( @@ -123,6 +139,7 @@ togglePermission, nextPage, previousPage, + isPermissionChecked, specialPermissionsDetected$, confirmDistributionRights$, previousPageAction$, @@ -146,6 +163,11 @@ required: false, default: false, }, + readOnly: { + type: Boolean, + required: false, + default: false, + }, }, emits: ['input', 'update:allChecked'], }; diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/Accordion.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/Accordion.vue new file mode 100644 index 0000000000..474b994053 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/Accordion.vue @@ -0,0 +1,70 @@ + + + + + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ChannelDetails.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ChannelDetails.vue new file mode 100644 index 0000000000..5372deb07e --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ChannelDetails.vue @@ -0,0 +1,279 @@ + + + + + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue index 48f746c33a..b9745ddd18 100644 --- a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue @@ -36,12 +36,12 @@
{{ channelVersionTokenLabel$() }}
@@ -69,6 +69,19 @@ />
+
+ @@ -81,6 +94,7 @@ import { computed, ref, watch } from 'vue'; import CommunityLibraryChip from '../CommunityLibraryChip.vue'; import CommunityLibraryStatusChip from '../CommunityLibraryStatusChip.vue'; + import ChannelDetails from './ChannelDetails.vue'; import StudioCopyToken from 'shared/views/StudioCopyToken/index.vue'; import StudioThumbnail from 'shared/views/files/StudioThumbnail.vue'; import { useFetch } from 'shared/composables/useFetch'; @@ -129,7 +143,7 @@ }); const closeModal = () => { - // tes + isModalOpen.value = false; }; const { @@ -139,17 +153,12 @@ } = useFetch({ asyncFetchFunc: async () => { try { - const channelPromise = store.dispatch('channel/loadChannel', props.channelId); - const channelDetailsPromise = store.dispatch('channel/loadChannelDetails', props.channelId); - const [channel, channelDetails] = await Promise.all([ - channelPromise, - channelDetailsPromise, - ]); + const channel = await store.dispatch('channel/loadChannel', props.channelId); if (!channel) { closeModal(); return; } - return { ...channel, ...channelDetails }; + return channel; } catch (error) { store.dispatch('errors/handleAxiosError', error); } diff --git a/contentcuration/contentcuration/frontend/shared/views/details/StudioDetailsPanel.vue b/contentcuration/contentcuration/frontend/shared/views/details/StudioDetailsPanel.vue index e8542cad76..79cc4921bd 100644 --- a/contentcuration/contentcuration/frontend/shared/views/details/StudioDetailsPanel.vue +++ b/contentcuration/contentcuration/frontend/shared/views/details/StudioDetailsPanel.vue @@ -4,32 +4,33 @@ :class="{ printing }" data-testid="details-panel" > - - -
-

- {{ _details.name }} -

-

- {{ _details.description }} -

-
- + +
+

+ {{ _details.name }} +

+

+ {{ _details.description }} +

+
+ -
+ - + - + - + @@ -251,7 +274,10 @@ - + @@ -455,6 +486,10 @@ type: Boolean, default: true, }, + hideChannelHeader: { + type: Boolean, + default: false, + }, }, computed: { _details() { @@ -502,6 +537,9 @@ return orderBy(this._details.tags, ['count'], ['desc']); }, includesPrintable() { + if (!this._details.includes) { + return this.defaultText; + } const includes = []; if (this._details.includes.coach_content) { includes.push(this.$tr('coachHeading')); @@ -514,12 +552,15 @@ return includes.length ? includes.join(', ') : this.defaultText; }, licensesPrintable() { - return this._details.licenses.map(this.translateConstant).join(', '); + return this._details.licenses?.map(this.translateConstant).join(', ') || this.defaultText; }, tagPrintable() { return this.sortedTags.map(tag => tag.tag_name).join(', '); }, levels() { + if (!this._details.levels) { + return null; + } return this._details.levels.map(level => { level = LevelsLookup[level]; let translationKey; @@ -536,16 +577,19 @@ }); }, levelsPrintable() { - return this.levels.join(', '); + return this.levels?.join(', ') || this.defaultText; }, categories() { + if (!this._details.categories) { + return null; + } return this._details.categories.map(category => { category = CategoriesLookup[category]; return this.translateMetadataString(camelCase(category)); }); }, categoriesPrintable() { - return this.categories.join(', '); + return this.categories?.join(', ') || this.defaultText; }, }, methods: { @@ -660,4 +704,8 @@ word-break: break-word; } + .sample-wrapper { + grid-column: 1 / -1; + } + diff --git a/contentcuration/contentcuration/viewsets/channel.py b/contentcuration/contentcuration/viewsets/channel.py index ae0cb760c0..353ad4e17d 100644 --- a/contentcuration/contentcuration/viewsets/channel.py +++ b/contentcuration/contentcuration/viewsets/channel.py @@ -1060,6 +1060,9 @@ class ChannelVersionViewSet(ReadOnlyValuesViewset): "id", "channel", "version", + "size", + "resource_count", + "kind_count", "date_published", "version_notes", "included_languages", From 677893ff2e0bec9f9f005f428da077c67e7c8bec Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Mon, 23 Feb 2026 08:25:23 -0500 Subject: [PATCH 03/13] Add ActivityHistory component --- .../strings/communityChannelsStrings.js | 31 ++ .../frontend/shared/utils/communityLibrary.js | 23 +- .../composables/useCommunityLibraryUpdates.js | 13 +- .../ActivityHistory.vue | 278 ++++++++++++++++++ .../SubmissionDetailsModal/index.vue | 17 +- 5 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ActivityHistory.vue diff --git a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js index ddfb5ac4fd..491cdf2c3c 100644 --- a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js +++ b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js @@ -461,4 +461,35 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin message: 'Live version:', context: 'Label indicating the live version of a channel', }, + activityHistoryLabel: { + message: 'Activity history', + context: 'Label for the activity history section in the submission details page', + }, + + // Resolution reasons strings + reasonLabel: { + message: 'Reason: {reason}', + context: 'Label for the reason provided for a given action (e.g., rejection reason)', + }, + invalidLicensingReason: { + message: 'Invalid or non-compliant licenses', + context: 'Rejection reason indicating that the channel has invalid or non-compliant licenses', + }, + qualityAssuranceReason: { + message: 'Quality assurance issues', + context: 'Rejection reason indicating that the channel has quality assurance issues', + }, + invalidMetadataReason: { + message: 'Invalid or missing metadata', + context: 'Rejection reason indicating that the channel has invalid or missing metadata', + }, + portabilityIssuesReason: { + message: 'Portability problems', + context: 'Rejection reason indicating that the channel has portability problems', + }, + otherIssuesReason: { + message: 'Other issues', + context: + 'Rejection reason indicating that the channel has other issues not covered by other reasons', + }, }); diff --git a/contentcuration/contentcuration/frontend/shared/utils/communityLibrary.js b/contentcuration/contentcuration/frontend/shared/utils/communityLibrary.js index 4449c3111b..f07d658a67 100644 --- a/contentcuration/contentcuration/frontend/shared/utils/communityLibrary.js +++ b/contentcuration/contentcuration/frontend/shared/utils/communityLibrary.js @@ -1,4 +1,5 @@ -import { CommunityLibraryStatus } from 'shared/constants'; +import { CommunityLibraryResolutionReason, CommunityLibraryStatus } from 'shared/constants'; +import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings'; export const getUiSubmissionStatus = status => { // We do not need to distinguish LIVE from APPROVED in many parts of the UI @@ -9,3 +10,23 @@ export const getUiSubmissionStatus = status => { }; return uiStatusMap[status] || status; }; + +export const getResolutionReasonLabel = reason => { + const { + invalidLicensingReason$, + qualityAssuranceReason$, + invalidMetadataReason$, + portabilityIssuesReason$, + otherIssuesReason$, + } = communityChannelsStrings; + + const reasonLabelMap = { + [CommunityLibraryResolutionReason.INVALID_LICENSING]: invalidLicensingReason$(), + [CommunityLibraryResolutionReason.TECHNICAL_QUALITY_ASSURANCE]: qualityAssuranceReason$(), + [CommunityLibraryResolutionReason.INVALID_METADATA]: invalidMetadataReason$(), + [CommunityLibraryResolutionReason.PORTABILITY_ISSUES]: portabilityIssuesReason$(), + [CommunityLibraryResolutionReason.OTHER]: otherIssuesReason$(), + }; + + return reasonLabelMap[reason] || reason; +}; diff --git a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js index 7bc563cb7a..04b9e6579f 100644 --- a/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js +++ b/contentcuration/contentcuration/frontend/shared/views/NotificationsModal/composables/useCommunityLibraryUpdates.js @@ -78,17 +78,22 @@ export default function useCommunityLibraryUpdates({ queryParams } = {}) { date_updated: submission.date_updated && new Date(submission.date_updated), }; + // If the status is not PENDING or SUPERSEDED, it means there is also a status update + const hasUpdate = ![ + CommunityLibraryStatus.PENDING, + CommunityLibraryStatus.SUPERSEDED, + ].includes(sub.status); + // Always add creation update updates.push({ ...sub, + // If it has updates, it means the current status is not the initial creation status + status: hasUpdate ? CommunityLibraryStatus.PENDING : sub.status, type: NotificationType.COMMUNITY_LIBRARY_SUBMISSION_CREATED, date: sub.date_created, }); - // If the status is not PENDING or SUPERSEDED, it means there is also a status update - if ( - ![CommunityLibraryStatus.PENDING, CommunityLibraryStatus.SUPERSEDED].includes(sub.status) - ) { + if (hasUpdate) { updates.push({ ...sub, type: statusToNotificationType[sub.status], diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ActivityHistory.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ActivityHistory.vue new file mode 100644 index 0000000000..a0edb01304 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ActivityHistory.vue @@ -0,0 +1,278 @@ + + + + + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue index b9745ddd18..e5e1b991c4 100644 --- a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/index.vue @@ -22,7 +22,12 @@ class="notranslate" dir="auto" > - {{ channelVersion$({ name: channel.name, version: submission.channel_version }) }} + {{ + channelVersion$({ + name: channel.name, + version: submission.channel_version, + }) + }}

+

@@ -95,6 +105,7 @@ import CommunityLibraryChip from '../CommunityLibraryChip.vue'; import CommunityLibraryStatusChip from '../CommunityLibraryStatusChip.vue'; import ChannelDetails from './ChannelDetails.vue'; + import ActivityHistory from './ActivityHistory.vue'; import StudioCopyToken from 'shared/views/StudioCopyToken/index.vue'; import StudioThumbnail from 'shared/views/files/StudioThumbnail.vue'; import { useFetch } from 'shared/composables/useFetch'; @@ -308,4 +319,8 @@ } } + .mt-16 { + margin-top: 16px; + } + From 48db1b75be1fdf13cfbc6681d3a2b349176546ed Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Mon, 23 Feb 2026 09:39:40 -0500 Subject: [PATCH 04/13] Add activity line --- .../ActivityHistory.vue | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ActivityHistory.vue b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ActivityHistory.vue index a0edb01304..b3a4f5fc4a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ActivityHistory.vue +++ b/contentcuration/contentcuration/frontend/shared/views/communityLibrary/SubmissionDetailsModal/ActivityHistory.vue @@ -18,11 +18,6 @@
  • {{ getUpdateTitle(update) }} @@ -220,6 +215,12 @@