From bee3758d188e382a77ab2d032f8dbe79bcf09b0c Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:18:03 -0500 Subject: [PATCH] feat: update usage location call (#764) --- .../table-custom-columns/ActiveColumn.jsx | 25 ++++++++++++++++--- .../videos-page/VideosPage.jsx | 2 +- .../videos-page/VideosPage.test.jsx | 23 ++++++++++++++--- src/files-and-videos/videos-page/data/api.js | 1 + .../videos-page/data/thunks.js | 25 ++++++++++++++++--- .../videos-page/data/utils.js | 7 +++--- .../pacing-section/PacingSection.test.jsx | 3 ++- 7 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/files-and-videos/generic/table-components/table-custom-columns/ActiveColumn.jsx b/src/files-and-videos/generic/table-components/table-custom-columns/ActiveColumn.jsx index 149c0736ef..84fdf127bc 100644 --- a/src/files-and-videos/generic/table-components/table-custom-columns/ActiveColumn.jsx +++ b/src/files-and-videos/generic/table-components/table-custom-columns/ActiveColumn.jsx @@ -1,11 +1,30 @@ import React from 'react'; import { PropTypes } from 'prop-types'; -import { Icon } from '@edx/paragon'; +import { isNil } from 'lodash'; +import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Icon, Spinner } from '@edx/paragon'; import { Check } from '@edx/paragon/icons'; const ActiveColumn = ({ row }) => { const { usageLocations } = row.original; - const numOfUsageLocations = usageLocations?.length; + if (isNil(usageLocations)) { + return ( + + )} + /> + ); + } + const numOfUsageLocations = usageLocations.length; return numOfUsageLocations > 0 ? : null; }; @@ -17,4 +36,4 @@ ActiveColumn.propTypes = { }.isRequired, }; -export default ActiveColumn; +export default injectIntl(ActiveColumn); diff --git a/src/files-and-videos/videos-page/VideosPage.jsx b/src/files-and-videos/videos-page/VideosPage.jsx index 3b4fe807f8..852c5c1760 100644 --- a/src/files-and-videos/videos-page/VideosPage.jsx +++ b/src/files-and-videos/videos-page/VideosPage.jsx @@ -80,7 +80,7 @@ const VideosPage = ({ const supportedFileFormats = { 'video/*': videoSupportedFileFormats || FILES_AND_UPLOAD_TYPE_FILTERS.video }; - const handleAddFile = (file) => dispatch(addVideoFile(courseId, file)); + const handleAddFile = (file) => dispatch(addVideoFile(courseId, file, videoIds)); const handleDeleteFile = (id) => dispatch(deleteVideoFile(courseId, id)); const handleDownloadFile = (selectedRows) => dispatch(fetchVideoDownload({ selectedRows, courseId })); const handleUsagePaths = (video) => dispatch(getUsagePaths({ video, courseId })); diff --git a/src/files-and-videos/videos-page/VideosPage.test.jsx b/src/files-and-videos/videos-page/VideosPage.test.jsx index 0168b1a4af..b4ce8ca07c 100644 --- a/src/files-and-videos/videos-page/VideosPage.test.jsx +++ b/src/files-and-videos/videos-page/VideosPage.test.jsx @@ -59,7 +59,13 @@ const mockStore = async ( status, ) => { const fetchVideosUrl = getVideosUrl(courseId); - axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), generateFetchVideosApiResponse()); + const videosData = generateFetchVideosApiResponse(); + axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), videosData); + + videosData.previous_uploads.forEach((video) => { + axiosMock.onGet(`${getVideosUrl(courseId)}/${video.edx_video_id}/usage`).reply(200, { usageLocations: [] }); + }); + renderComponent(); await executeThunk(fetchVideos(courseId), store.dispatch); }; @@ -71,7 +77,7 @@ const emptyMockStore = async (status) => { await executeThunk(fetchVideos(courseId), store.dispatch); }; -describe('FilesAndUploads', () => { +describe('Videos page', () => { describe('empty state', () => { beforeEach(async () => { initializeMockApp({ @@ -179,6 +185,13 @@ describe('FilesAndUploads', () => { it('should switch table to list view', async () => { await mockStore(RequestStatus.SUCCESSFUL); + axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`) + .reply(201, { + usageLocations: [{ + display_location: 'subsection - unit / block', + url: 'base/unit_id#block_id', + }], + }); expect(screen.getByTestId('files-data-table')).toBeVisible(); expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible(); @@ -219,9 +232,10 @@ describe('FilesAndUploads', () => { axiosMock.onGet(getCourseVideosApiUrl(courseId)).reply(200, generateAddVideoApiResponse()); const addFilesButton = screen.getAllByLabelText('file-input')[3]; + const { videoIds } = store.getState().videos; await act(async () => { userEvent.upload(addFilesButton, file); - await executeThunk(addVideoFile(courseId, file), store.dispatch); + await executeThunk(addVideoFile(courseId, file, videoIds), store.dispatch); }); const addStatus = store.getState().videos.addingStatus; expect(addStatus).toEqual(RequestStatus.SUCCESSFUL); @@ -419,12 +433,13 @@ describe('FilesAndUploads', () => { const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1'); axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`) - .reply(201, { + .reply(200, { usageLocations: [{ display_location: 'subsection - unit / block', url: 'base/unit_id#block_id', }], }); + await waitFor(() => { fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle')); fireEvent.click(screen.getByText('Info')); diff --git a/src/files-and-videos/videos-page/data/api.js b/src/files-and-videos/videos-page/data/api.js index a757dcf9a8..be38613447 100644 --- a/src/files-and-videos/videos-page/data/api.js +++ b/src/files-and-videos/videos-page/data/api.js @@ -177,6 +177,7 @@ export async function uploadVideo( const formData = new FormData(); formData.append('uploaded-file', uploadFile); const uploadErrors = []; + await fetch(uploadUrl, { method: 'PUT', body: formData, diff --git a/src/files-and-videos/videos-page/data/thunks.js b/src/files-and-videos/videos-page/data/thunks.js index 245ffd0bba..713e8ce0d8 100644 --- a/src/files-and-videos/videos-page/data/thunks.js +++ b/src/files-and-videos/videos-page/data/thunks.js @@ -5,7 +5,6 @@ import { addModels, removeModel, updateModel, - updateModels, } from '../../../generic/model-store'; import { addThumbnail, @@ -38,6 +37,20 @@ import { import { updateFileValues } from './utils'; +async function fetchUsageLocation(videoId, dispatch, courseId) { + const { usageLocations } = await getVideoUsagePaths({ videoId, courseId }); + const activeStatus = usageLocations?.length > 0 ? 'active' : 'inactive'; + + dispatch(updateModel({ + modelType: 'videos', + model: { + id: videoId, + usageLocations, + activeStatus, + }, + })); +} + export function fetchVideos(courseId) { return async (dispatch) => { dispatch(updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS })); @@ -51,6 +64,9 @@ export function fetchVideos(courseId) { })); dispatch(setPageSettings({ ...data })); dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL })); + parsedVideos.forEach(async (video) => { + fetchUsageLocation(video.id, dispatch, courseId); + }); } catch (error) { if (error.response && error.response.status === 403) { dispatch(updateLoadingStatus({ status: RequestStatus.DENIED })); @@ -90,7 +106,7 @@ export function deleteVideoFile(courseId, id) { }; } -export function addVideoFile(courseId, file) { +export function addVideoFile(courseId, file, videoIds) { return async (dispatch) => { dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.IN_PROGRESS })); @@ -104,8 +120,9 @@ export function addVideoFile(courseId, file) { edxVideoId, ); const { videos } = await fetchVideoList(courseId); - const parsedVideos = updateFileValues(videos); - dispatch(updateModels({ + const newVideos = videos.filter(video => !videoIds.includes(video.edxVideoId)); + const parsedVideos = updateFileValues(newVideos, true); + dispatch(addModels({ modelType: 'videos', models: parsedVideos, })); diff --git a/src/files-and-videos/videos-page/data/utils.js b/src/files-and-videos/videos-page/data/utils.js index 4d9408607b..a19c8fe1b6 100644 --- a/src/files-and-videos/videos-page/data/utils.js +++ b/src/files-and-videos/videos-page/data/utils.js @@ -15,7 +15,7 @@ ensureConfig([ 'STUDIO_BASE_URL', ], 'Course Apps API service'); -export const updateFileValues = (files) => { +export const updateFileValues = (files, isNewFile) => { const updatedFiles = []; files.forEach(file => { const { @@ -25,7 +25,6 @@ export const updateFileValues = (files) => { courseVideoImageUrl, status, transcripts, - usageLocations, } = file; const wrapperType = 'video'; @@ -34,7 +33,6 @@ export const updateFileValues = (files) => { thumbnail = `${getConfig().STUDIO_BASE_URL}${thumbnail}`; } const transcriptStatus = transcripts?.length > 0 ? 'transcribed' : 'notTranscribed'; - const activeStatus = usageLocations?.length > 0 ? 'active' : 'inactive'; let uploadStatus = status; if (VIDEO_SUCCESS_STATUSES.includes(status)) { @@ -49,10 +47,11 @@ export const updateFileValues = (files) => { id: edxVideoId, wrapperType, dateAdded: created.toString(), + usageLocations: isNewFile ? [] : null, status: uploadStatus, thumbnail, transcriptStatus, - activeStatus, + activeStatus: 'inactive', }); }); diff --git a/src/schedule-and-details/pacing-section/PacingSection.test.jsx b/src/schedule-and-details/pacing-section/PacingSection.test.jsx index c8bacc17f1..4bbeca6baa 100644 --- a/src/schedule-and-details/pacing-section/PacingSection.test.jsx +++ b/src/schedule-and-details/pacing-section/PacingSection.test.jsx @@ -43,7 +43,8 @@ describe('', () => { }); it('shows disabled radio inputs correctly', () => { - const pastDate = '2024-12-31'; + const year = new Date().getFullYear() + 1; + const pastDate = `${year}-12-31`; const initialProps = { ...props, startDate: pastDate }; const { getAllByRole, queryAllByText } = render( ,