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(
,