diff --git a/contentcuration/contentcuration/frontend/administration/components/sidePanels/ReviewSubmissionSidePanel.vue b/contentcuration/contentcuration/frontend/administration/components/sidePanels/ReviewSubmissionSidePanel.vue new file mode 100644 index 0000000000..22d1252378 --- /dev/null +++ b/contentcuration/contentcuration/frontend/administration/components/sidePanels/ReviewSubmissionSidePanel.vue @@ -0,0 +1,659 @@ + + + + + + + diff --git a/contentcuration/contentcuration/frontend/administration/components/sidePanels/__tests__/ReviewSubmissionSidePanel.spec.js b/contentcuration/contentcuration/frontend/administration/components/sidePanels/__tests__/ReviewSubmissionSidePanel.spec.js new file mode 100644 index 0000000000..3a6e5e04fd --- /dev/null +++ b/contentcuration/contentcuration/frontend/administration/components/sidePanels/__tests__/ReviewSubmissionSidePanel.spec.js @@ -0,0 +1,503 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import { factory } from '../../../store'; +import router from '../../../router'; + +import ReviewSubmissionSidePanel from '../ReviewSubmissionSidePanel'; +import { + Categories, + CommunityLibraryResolutionReason, + CommunityLibraryStatus, +} from 'shared/constants'; +import { ChannelVersion, AdminCommunityLibrarySubmission } from 'shared/data/resources'; +import CommunityLibraryStatusChip from 'shared/views/communityLibrary/CommunityLibraryStatusChip.vue'; + +jest.mock('shared/data/resources', () => ({ + AdminCommunityLibrarySubmission: { + fetchModel: jest.fn(), + resolve: jest.fn(() => Promise.resolve()), + }, + ChannelVersion: { + fetchCollection: jest.fn(), + }, + AuditedSpecialPermissionsLicense: { + fetchCollection: jest.fn(() => Promise.resolve([])), + }, +})); + +const store = factory(); + +async function makeWrapper({ channel, latestSubmission, submissionFetch }) { + AdminCommunityLibrarySubmission.fetchModel.mockImplementation( + submissionFetch || + (() => { + return Promise.resolve(latestSubmission); + }), + ); + + ChannelVersion.fetchCollection.mockImplementation(() => { + return Promise.resolve([channelVersionData]); + }); + + const wrapper = mount(ReviewSubmissionSidePanel, { + store, + router, + propsData: { + channel, + }, + mocks: { + $formatRelative: data => + Vue.prototype.$formatRelative(data, { now: new Date('2024-01-01T00:15:00Z') }), + }, + }); + + // To simulate that first the data is loading and then it finishes loading + // and correctly trigger watchers depending on that + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + return wrapper; +} + +const channelCommon = { + id: 'channel-id', + name: 'Test Channel', + published_data: { + 1: { + included_languages: ['en'], + included_licenses: [1], + included_categories: [Categories.SCHOOL], + }, + 2: { + included_languages: ['en', 'cs'], + included_licenses: [1, 2], + included_categories: [Categories.SCHOOL, Categories.ALGEBRA], + }, + 3: { + included_languages: ['en'], + included_licenses: [1], + included_categories: [Categories.SCHOOL], + }, + }, + version: 3, +}; + +const submissionCommon = { + id: 'submission-id', + author_name: 'Test Author', + description: 'Author description', + date_created: '2024-01-01T00:00:00Z', + channel_name: 'Test Channel', + channel_version: 2, + countries: ['US', 'CZ'], + categories: [Categories.SCHOOL, Categories.ALGEBRA], + resolution_reason: null, + feedback_notes: null, + internal_notes: null, +}; + +const channelVersionData = channelCommon.published_data[submissionCommon.channel_version]; + +const testData = { + submitted: { + channel: { + ...channelCommon, + latest_community_library_submission_status: CommunityLibraryStatus.PENDING, + }, + submission: { + ...submissionCommon, + status: CommunityLibraryStatus.PENDING, + }, + }, + approved: { + channel: { + ...channelCommon, + latest_community_library_submission_status: CommunityLibraryStatus.APPROVED, + }, + submission: { + ...submissionCommon, + status: CommunityLibraryStatus.APPROVED, + feedback_notes: 'Feedback notes', + internal_notes: 'Internal notes', + }, + }, + flagged: { + channel: { + ...channelCommon, + latest_community_library_submission_status: CommunityLibraryStatus.REJECTED, + }, + submission: { + ...submissionCommon, + status: CommunityLibraryStatus.REJECTED, + resolution_reason: CommunityLibraryResolutionReason.PORTABILITY_ISSUES, + feedback_notes: 'Feedback notes', + internal_notes: 'Internal notes', + }, + }, +}; + +describe('ReviewSubmissionSidePanel', () => { + it('submission data is prefilled', async () => { + const { channel, submission } = testData.flagged; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + await wrapper.vm.$nextTick(); + expect(wrapper.find('.author-name').text()).toBe(submission.author_name); + expect(wrapper.find('.channel-link').text()).toBe( + `${channel.name} v${submission.channel_version}`, + ); + expect(wrapper.find('[data-test="submission-date"]').text()).toBe('15 minutes ago'); + expect(wrapper.find('[data-test="countries"]').text()).toBe( + 'United States of America, Czech Republic', + ); + expect(wrapper.find('[data-test="languages"]').text()).toBe('English, Czech'); + expect(wrapper.find('[data-test="categories"]').text()).toBe('School, Algebra'); + expect(wrapper.find('[data-test="licenses"]').text()).toBe('CC BY, CC BY-SA'); + expect(wrapper.findComponent(CommunityLibraryStatusChip).props('status')).toEqual( + submission.status, + ); + expect(wrapper.find('[data-test="submission-notes"]').text()).toBe(submission.description); + expect(wrapper.find(`input[type="radio"][value="${submission.status}"]`).element.checked).toBe( + true, + ); + expect(wrapper.findComponent({ ref: 'flagReasonSelectRef' }).vm.value.value).toBe( + submission.resolution_reason, + ); + expect(wrapper.findComponent({ ref: 'editorNotesRef' }).vm.value).toBe( + submission.feedback_notes, + ); + expect(wrapper.findComponent({ ref: 'personalNotesRef' }).vm.value).toBe( + submission.internal_notes, + ); + }); + + describe('resolution reason is', () => { + it('displayed when selected status is flagged', async () => { + const { channel, submission } = testData.flagged; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + expect(wrapper.findComponent({ ref: 'flagReasonSelectRef' }).exists()).toBe(true); + }); + + it('hidden when selected status is approved', async () => { + const { channel, submission } = testData.approved; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + expect(wrapper.findComponent({ ref: 'flagReasonSelectRef' }).exists()).toBe(false); + }); + + it('hidden when selected status is submitted', async () => { + const { channel, submission } = testData.submitted; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + expect(wrapper.findComponent({ ref: 'flagReasonSelectRef' }).exists()).toBe(false); + }); + }); + + it('is editable when loading latest submission data has finished and when the submission state is submitted', async () => { + const { channel, submission } = testData.submitted; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + const flagForReviewRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.REJECTED}"]`, + ); + await flagForReviewRadio.trigger('click'); + + expect(flagForReviewRadio.attributes('disabled')).toBeFalsy(); + expect(wrapper.findComponent({ ref: 'flagReasonSelectRef' }).props('disabled')).toBe(false); + expect(wrapper.findComponent({ ref: 'editorNotesRef' }).props('disabled')).toBe(false); + expect(wrapper.findComponent({ ref: 'personalNotesRef' }).props('disabled')).toBe(false); + }); + + describe('is not editable', () => { + it('when submission data is still loading', async () => { + const { channel } = testData.submitted; + const mockSubmissionFetch = () => { + return new Promise(() => { + // Never resolves to simulate loading state + }); + }; + const wrapper = await makeWrapper({ channel, submissionFetch: mockSubmissionFetch }); + + await wrapper.vm.$nextTick(); + + const flagForReviewRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.REJECTED}"]`, + ); + await flagForReviewRadio.trigger('click'); + + expect(flagForReviewRadio.attributes('disabled')).toBeTruthy(); + expect(wrapper.findComponent({ ref: 'editorNotesRef' }).props('disabled')).toBe(true); + expect(wrapper.findComponent({ ref: 'personalNotesRef' }).props('disabled')).toBe(true); + }); + + it('when the submission is approved', async () => { + const { channel, submission } = testData.approved; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + const flagForReviewRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.REJECTED}"]`, + ); + await flagForReviewRadio.trigger('click'); + + expect(flagForReviewRadio.attributes('disabled')).toBeTruthy(); + expect(wrapper.findComponent({ ref: 'editorNotesRef' }).props('disabled')).toBe(true); + expect(wrapper.findComponent({ ref: 'personalNotesRef' }).props('disabled')).toBe(true); + }); + + it('when the submission is flagged', async () => { + const { channel, submission } = testData.flagged; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + const flagForReviewRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.REJECTED}"]`, + ); + await flagForReviewRadio.trigger('click'); + + expect(flagForReviewRadio.attributes('disabled')).toBeTruthy(); + expect(wrapper.findComponent({ ref: 'editorNotesRef' }).props('disabled')).toBe(true); + expect(wrapper.findComponent({ ref: 'personalNotesRef' }).props('disabled')).toBe(true); + }); + }); + + describe('can be submitted', () => { + it('when selected status is approved', async () => { + const { channel, submission } = testData.submitted; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + const approveRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.APPROVED}"]`, + ); + await approveRadio.trigger('click'); + + expect(wrapper.findComponent({ ref: 'confirmButtonRef' }).props('disabled')).toBe(false); + }); + + it("when selected status is flagged and editor's notes are provided", async () => { + const { channel, submission } = testData.submitted; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + const flagForReviewRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.REJECTED}"]`, + ); + await flagForReviewRadio.trigger('click'); + + const editorNotes = wrapper.findComponent({ ref: 'editorNotesRef' }); + await editorNotes.vm.$emit('input', 'Some editor notes'); + + expect(wrapper.findComponent({ ref: 'confirmButtonRef' }).props('disabled')).toBe(false); + }); + }); + + describe('cannot be submitted', () => { + it('when submission data is still loading', async () => { + const { channel } = testData.submitted; + const mockSubmissionFetch = () => { + return new Promise(() => { + // Never resolves to simulate loading state + }); + }; + const wrapper = await makeWrapper({ channel, submissionFetch: mockSubmissionFetch }); + + await wrapper.vm.$nextTick(); + + const approveRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.APPROVED}"]`, + ); + await approveRadio.trigger('click'); + + expect(wrapper.findComponent({ ref: 'confirmButtonRef' }).props('disabled')).toBe(true); + }); + + it("when selected status is flagged and editor's notes are not provided", async () => { + const { channel, submission } = testData.submitted; + const wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + const flagForReviewRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.REJECTED}"]`, + ); + await flagForReviewRadio.trigger('click'); + + expect(wrapper.findComponent({ ref: 'confirmButtonRef' }).props('disabled')).toBe(true); + }); + }); + + describe('when submit button is clicked', () => { + let channel, submission, wrapper; + + beforeEach(async () => { + AdminCommunityLibrarySubmission.resolve.mockClear(); + + const { channel: _channel, submission: _submission } = testData.submitted; + channel = _channel; + submission = _submission; + + wrapper = await makeWrapper({ channel, latestSubmission: submission }); + + const approveRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.APPROVED}"]`, + ); + await approveRadio.trigger('click'); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('the panel closes', async () => { + jest.useFakeTimers(); + + const confirmButton = wrapper.findComponent({ ref: 'confirmButtonRef' }); + await confirmButton.trigger('click'); + + expect(wrapper.emitted('close')).toBeTruthy(); + }); + + it('a submission snackbar is shown', async () => { + jest.useFakeTimers(); + const confirmButton = wrapper.findComponent({ ref: 'confirmButtonRef' }); + await confirmButton.trigger('click'); + + expect(store.getters['snackbarIsVisible']).toBe(true); + expect(AdminCommunityLibrarySubmission.resolve).not.toHaveBeenCalled(); + }); + + describe('after a timeout', () => { + describe('if the submission is approved', () => { + it('the submission is correctly resolved', async () => { + jest.useFakeTimers(); + + const feedbackNotes = 'Some feedback notes'; + const feedbackNotesComponent = wrapper.findComponent({ ref: 'editorNotesRef' }); + await feedbackNotesComponent.vm.$emit('input', feedbackNotes); + + const personalNotes = 'Some personal notes'; + const personalNotesComponent = wrapper.findComponent({ ref: 'personalNotesRef' }); + await personalNotesComponent.vm.$emit('input', personalNotes); + + const confirmButton = wrapper.findComponent({ ref: 'confirmButtonRef' }); + await confirmButton.trigger('click'); + + jest.runAllTimers(); + await wrapper.vm.$nextTick(); + + expect(AdminCommunityLibrarySubmission.resolve).toHaveBeenCalledWith(submission.id, { + status: CommunityLibraryStatus.APPROVED, + feedback_notes: feedbackNotes, + internal_notes: personalNotes, + }); + }); + + it('a snackbar with correct status is shown', async () => { + const origStoreState = store.state; + store.commit('channel/ADD_CHANNEL', channel); + + jest.useFakeTimers(); + const confirmButton = wrapper.findComponent({ ref: 'confirmButtonRef' }); + await confirmButton.trigger('click'); + + jest.runAllTimers(); + await wrapper.vm.$nextTick(); + + expect(store.getters['snackbarOptions'].text).toBe('Submission approved'); + store.replaceState(origStoreState); + }); + + it('the channel latest submission status is updated in the store', async () => { + const origStoreState = store.state; + store.commit('channel/ADD_CHANNEL', channel); + + jest.useFakeTimers(); + const confirmButton = wrapper.findComponent({ ref: 'confirmButtonRef' }); + await confirmButton.trigger('click'); + + jest.runAllTimers(); + await wrapper.vm.$nextTick(); + + expect( + store.getters['channel/getChannel'](channel.id) + .latest_community_library_submission_status, + ).toBe(CommunityLibraryStatus.APPROVED); + store.replaceState(origStoreState); + }); + }); + + describe('if the submission is flagged for review', () => { + const feedbackNotes = 'Some feedback notes'; + const personalNotes = 'Some personal notes'; + + beforeEach(async () => { + const flagForReviewRadio = wrapper.find( + `input[type="radio"][value="${CommunityLibraryStatus.REJECTED}"]`, + ); + await flagForReviewRadio.trigger('click'); + + const flagReasonComponent = wrapper.findComponent({ ref: 'flagReasonSelectRef' }); + await flagReasonComponent.vm.setValue({ + value: CommunityLibraryResolutionReason.INVALID_METADATA, + text: 'Invalid or missing metadata', + }); + + const feedbackNotesComponent = wrapper.findComponent({ ref: 'editorNotesRef' }); + await feedbackNotesComponent.vm.$emit('input', feedbackNotes); + + const personalNotesComponent = wrapper.findComponent({ ref: 'personalNotesRef' }); + await personalNotesComponent.vm.$emit('input', personalNotes); + }); + + it('the submission is correctly resolved', async () => { + jest.useFakeTimers(); + + const confirmButton = wrapper.findComponent({ ref: 'confirmButtonRef' }); + await confirmButton.trigger('click'); + + jest.runAllTimers(); + await wrapper.vm.$nextTick(); + + expect(AdminCommunityLibrarySubmission.resolve).toHaveBeenCalledWith(submission.id, { + status: CommunityLibraryStatus.REJECTED, + feedback_notes: feedbackNotes, + internal_notes: personalNotes, + resolution_reason: CommunityLibraryResolutionReason.INVALID_METADATA, + }); + }); + + it('a snackbar with correct status is shown', async () => { + const origStoreState = store.state; + store.commit('channel/ADD_CHANNEL', channel); + + jest.useFakeTimers(); + + const confirmButton = wrapper.findComponent({ ref: 'confirmButtonRef' }); + await confirmButton.trigger('click'); + + jest.runAllTimers(); + await wrapper.vm.$nextTick(); + + expect(store.getters['snackbarOptions'].text).toBe('Submission flagged for review'); + store.replaceState(origStoreState); + }); + + it('the channel latest submission status is updated in the store', async () => { + const origStoreState = store.state; + store.commit('channel/ADD_CHANNEL', channel); + + jest.useFakeTimers(); + const confirmButton = wrapper.findComponent({ ref: 'confirmButtonRef' }); + await confirmButton.trigger('click'); + + jest.runAllTimers(); + await wrapper.vm.$nextTick(); + jest.runAllTimers(); + await wrapper.vm.$nextTick(); + + expect( + store.getters['channel/getChannel'](channel.id) + .latest_community_library_submission_status, + ).toBe(CommunityLibraryStatus.REJECTED); + store.replaceState(origStoreState); + }); + }); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelItem.vue b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelItem.vue index 54251994e8..8aa1300d1e 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelItem.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelItem.vue @@ -201,6 +201,7 @@ @@ -210,6 +211,12 @@ flat /> + @@ -219,12 +226,13 @@ import { mapGetters, mapActions } from 'vuex'; import ClipboardChip from '../../components/ClipboardChip'; + import ReviewSubmissionSidePanel from '../../components/sidePanels/ReviewSubmissionSidePanel'; import CommunityLibraryStatusButton from '../../components/CommunityLibraryStatusButton.vue'; import { RouteNames } from '../../constants'; import ChannelActionsDropdown from './ChannelActionsDropdown'; import { fileSizeMixin } from 'shared/mixins'; import Checkbox from 'shared/views/form/Checkbox'; - import { CommunityLibraryStatus } from 'shared/constants'; + import { getUiSubmissionStatus } from 'shared/utils/communityLibrary'; export default { name: 'ChannelItem', @@ -233,6 +241,7 @@ ClipboardChip, Checkbox, CommunityLibraryStatusButton, + ReviewSubmissionSidePanel, }, mixins: [fileSizeMixin], props: { @@ -245,6 +254,11 @@ required: true, }, }, + data() { + return { + submissionToReview: null, + }; + }, computed: { ...mapGetters('channel', ['getChannel']), selected: { @@ -285,14 +299,7 @@ }; }, communityLibraryStatus() { - // We do not need to distinguish LIVE from APPROVED in the UI - if ( - this.channel.latest_community_library_submission_status === CommunityLibraryStatus.LIVE - ) { - return CommunityLibraryStatus.APPROVED; - } - - return this.channel.latest_community_library_submission_status; + return getUiSubmissionStatus(this.channel.latest_community_library_submission_status); }, }, methods: { diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelTable.vue b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelTable.vue index bda9332c74..5892f0558d 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelTable.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelTable.vue @@ -334,7 +334,7 @@ { text: 'Demo URL', value: 'demo_server_url', sortable: false }, { text: 'Source URL', value: 'source_url', sortable: false }, { - text: 'Community Library Status', + text: 'Latest community library submission', value: 'latest_community_library_submission_status', sortable: false, }, diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelItem.spec.js b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelItem.spec.js index ea03f7c05f..ca876160c6 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelItem.spec.js +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelItem.spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import CommunityLibraryStatusButton from '../../../components/CommunityLibraryStatusButton.vue'; +import ReviewSubmissionSidePanel from '../../../components/sidePanels/ReviewSubmissionSidePanel'; import router from '../../../router'; import { factory } from '../../../store'; import { RouteNames } from '../../../constants'; @@ -176,4 +177,18 @@ describe('channelItem', () => { expect(statusButton.props('status')).toBe(CommunityLibraryStatus.REJECTED); }); }); + + it('Clicking on the status button opens the review submission side panel', async () => { + wrapper.setData({ testedChannel: submittedChannel }); + await wrapper.vm.$nextTick(); + + const statusCell = wrapper.find('[data-test="community-library-status"]'); + const statusButton = statusCell.findComponent(CommunityLibraryStatusButton); + + expect(wrapper.findComponent(ReviewSubmissionSidePanel).exists()).toBe(false); + + await statusButton.trigger('click'); + + expect(wrapper.findComponent(ReviewSubmissionSidePanel).exists()).toBe(true); + }); }); diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js index 13f47630bf..c4fcb8ae8e 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js @@ -4,18 +4,19 @@ import { factory } from '../../../../store'; import SubmitToCommunityLibrarySidePanel from '../'; import Box from '../Box.vue'; -import StatusChip from '../StatusChip.vue'; -import { usePublishedData } from '../composables/usePublishedData'; -import { useLatestCommunityLibrarySubmission } from '../composables/useLatestCommunityLibrarySubmission'; +import { useVersionDetail } from '../composables/useVersionDetail'; +import { useLicenseAudit } from '../composables/useLicenseAudit'; +import { useLatestCommunityLibrarySubmission } from 'shared/composables/useLatestCommunityLibrarySubmission'; +import CommunityLibraryStatusChip from 'shared/views/communityLibrary/CommunityLibraryStatusChip.vue'; import { Categories, CommunityLibraryStatus } from 'shared/constants'; import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings'; import { CommunityLibrarySubmission } from 'shared/data/resources'; import CountryField from 'shared/views/form/CountryField.vue'; -jest.mock('../composables/usePublishedData'); -jest.mock('../composables/useLatestCommunityLibrarySubmission'); +jest.mock('../composables/useVersionDetail'); jest.mock('../composables/useLicenseAudit'); +jest.mock('shared/composables/useLatestCommunityLibrarySubmission'); jest.mock('shared/data/resources', () => ({ CommunityLibrarySubmission: { create: jest.fn(() => Promise.resolve()), @@ -24,6 +25,9 @@ jest.mock('shared/data/resources', () => ({ fetchModel: jest.fn(), getCatalogChannel: jest.fn(() => Promise.resolve()), }, + AuditedSpecialPermissionsLicense: { + fetchCollection: jest.fn(() => Promise.resolve([])), + }, })); const store = factory(); @@ -40,10 +44,10 @@ async function makeWrapper({ channel, publishedData, latestSubmission }) { store.state.currentChannel.currentChannelId = channel.id; store.commit('channel/ADD_CHANNEL', channel); - usePublishedData.mockReturnValue({ + useVersionDetail.mockReturnValue({ isLoading, isFinished, - data: computed(() => publishedData), + data: computed(() => ({ id: 'abcdabcdabcdabcdabcdabcdabcdabcd', ...publishedData })), fetchData: fetchPublishedData, }); @@ -54,6 +58,16 @@ async function makeWrapper({ channel, publishedData, latestSubmission }) { fetchData: fetchLatestSubmission, }); + useLicenseAudit.mockReturnValue({ + isLoading: computed(() => false), + isFinished: computed(() => true), + isAuditing: ref(false), + invalidLicenses: ref([]), + includedLicenses: ref([]), + hasAuditData: computed(() => true), + checkAndTriggerAudit: jest.fn(), + }); + const wrapper = mount(SubmitToCommunityLibrarySidePanel, { store, propsData: { @@ -377,7 +391,7 @@ describe('SubmitToCommunityLibrarySidePanel', () => { latestSubmission: null, }); - const statusChip = wrapper.findAllComponents(StatusChip); + const statusChip = wrapper.findAllComponents(CommunityLibraryStatusChip); expect(statusChip.exists()).toBe(false); }); @@ -389,7 +403,7 @@ describe('SubmitToCommunityLibrarySidePanel', () => { latestSubmission: { channel_version: 1, status: submissionStatus }, }); - const statusChip = wrapper.findComponent(StatusChip); + const statusChip = wrapper.findComponent(CommunityLibraryStatusChip); expect(statusChip.props('status')).toBe(chipStatus); }); } @@ -398,6 +412,7 @@ describe('SubmitToCommunityLibrarySidePanel', () => { testStatusChip(CommunityLibraryStatus.LIVE, CommunityLibraryStatus.APPROVED); testStatusChip(CommunityLibraryStatus.REJECTED, CommunityLibraryStatus.REJECTED); testStatusChip(CommunityLibraryStatus.PENDING, CommunityLibraryStatus.PENDING); + testStatusChip(CommunityLibraryStatus.SUPERSEDED, CommunityLibraryStatus.PENDING); }); it('is editable when channel is published, not public and not submitted', async () => { @@ -594,7 +609,7 @@ describe('SubmitToCommunityLibrarySidePanel', () => { description: 'Some description', channel: publishedNonPublicChannel.id, countries: ['CZ'], - categories: [Categories.SCHOOL], + categories: { [Categories.SCHOOL]: true }, }); jest.useRealTimers(); }); diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLatestCommunityLibrarySubmission.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLatestCommunityLibrarySubmission.js deleted file mode 100644 index ee65710b7f..0000000000 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLatestCommunityLibrarySubmission.js +++ /dev/null @@ -1,19 +0,0 @@ -import { useFetch } from '../../../../composables/useFetch'; -import { CommunityLibrarySubmission } from 'shared/data/resources'; - -export function useLatestCommunityLibrarySubmission(channelId) { - function fetchLatestSubmission() { - // Submissions are ordered by most recent first in the backend - return CommunityLibrarySubmission.fetchCollection({ channel: channelId, max_results: 1 }).then( - response => { - if (response.results.length > 0) { - return response.results[0]; - } - return null; - }, - ); - } - return useFetch({ - asyncFetchFunc: fetchLatestSubmission, - }); -} diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/usePublishedData.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useVersionDetail.js similarity index 55% rename from contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/usePublishedData.js rename to contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useVersionDetail.js index 9f35e56fa6..5d70248310 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/usePublishedData.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useVersionDetail.js @@ -1,6 +1,6 @@ -import { useFetch } from '../../../../composables/useFetch'; +import { useFetch } from 'shared/composables/useFetch'; import { Channel } from 'shared/data/resources'; -export function usePublishedData(channelId) { +export function useVersionDetail(channelId) { return useFetch({ asyncFetchFunc: () => Channel.getVersionDetail(channelId) }); } diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue index dcfe5b3e6a..37b1a6b5a5 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue @@ -61,7 +61,7 @@ {{ infoText }} - -
- {{ - channelVersion$({ - name: channel ? channel.name : '', - version: displayedVersion, - }) - }} -
-