Skip to content

Commit fdcda98

Browse files
authored
feat: include usage locations in delete modal (openedx#938)
1 parent 74eaaa1 commit fdcda98

File tree

7 files changed

+318
-44
lines changed

7 files changed

+318
-44
lines changed

src/files-and-videos/files-page/FilesPage.test.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ describe('FilesAndUploads', () => {
331331
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
332332

333333
fireEvent.click(deleteButton);
334-
expect(screen.getByText('Delete file(s) confirmation')).toBeVisible();
334+
expect(screen.getByText('Delete mOckID1')).toBeVisible();
335335
await act(async () => {
336336
userEvent.click(deleteButton);
337337
});
@@ -345,7 +345,7 @@ describe('FilesAndUploads', () => {
345345
userEvent.click(confirmDeleteButton);
346346
});
347347

348-
expect(screen.queryByText('Delete file(s) confirmation')).toBeNull();
348+
expect(screen.queryByText('Delete mOckID1')).toBeNull();
349349

350350
// Check if the asset is deleted in the store and UI
351351
const deleteStatus = store.getState().assets.deletingStatus;
@@ -554,10 +554,10 @@ describe('FilesAndUploads', () => {
554554
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
555555
fireEvent.click(within(assetMenuButton).getByLabelText('file-menu-toggle'));
556556
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
557-
expect(screen.getByText('Delete file(s) confirmation')).toBeVisible();
557+
expect(screen.getByText('Delete mOckID1')).toBeVisible();
558558

559559
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
560-
expect(screen.queryByText('Delete file(s) confirmation')).toBeNull();
560+
expect(screen.queryByText('Delete mOckID1')).toBeNull();
561561

562562
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
563563
});
@@ -644,10 +644,10 @@ describe('FilesAndUploads', () => {
644644
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID3`).reply(404);
645645
fireEvent.click(within(assetMenuButton).getByLabelText('file-menu-toggle'));
646646
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
647-
expect(screen.getByText('Delete file(s) confirmation')).toBeVisible();
647+
expect(screen.getByText('Delete mOckID3')).toBeVisible();
648648

649649
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
650-
expect(screen.queryByText('Delete file(s) confirmation')).toBeNull();
650+
expect(screen.queryByText('Delete mOckID3')).toBeNull();
651651

652652
executeThunk(deleteAssetFile(courseId, 'mOckID3', 5), store.dispatch);
653653
});

src/files-and-videos/files-page/messages.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,92 @@ const messages = defineMessages({
44
heading: {
55
id: 'course-authoring.files-and-uploads.heading',
66
defaultMessage: 'Files',
7+
description: 'Title for the page',
78
},
89
thumbnailAltMessage: {
910
id: 'course-authoring.files-and-uploads.thumbnail.alt',
1011
defaultMessage: '{displayName} file preview',
12+
description: 'Alternative text for thumbnail',
1113
},
1214
copyStudioUrlTitle: {
1315
id: 'course-authoring.files-and-uploads.file-info.copyStudioUrl.title',
1416
defaultMessage: 'Copy Studio Url',
17+
description: 'Label for Copy Studio URL button in info modal',
1518
},
1619
copyWebUrlTitle: {
1720
id: 'course-authoring.files-and-uploads.file-info.copyWebUrl.title',
1821
defaultMessage: 'Copy Web Url',
22+
description: 'Label for Copy Web URL button in info modal',
1923
},
2024
dateAddedTitle: {
2125
id: 'course-authoring.files-and-uploads.file-info.dateAdded.title',
2226
defaultMessage: 'Date added',
27+
description: 'Title for date added section in info modal',
2328
},
2429
fileSizeTitle: {
2530
id: 'course-authoring.files-and-uploads.file-info.fileSize.title',
2631
defaultMessage: 'File size',
32+
description: 'Title for file size section in info modal',
2733
},
2834
studioUrlTitle: {
2935
id: 'course-authoring.files-and-uploads.file-info.studioUrl.title',
3036
defaultMessage: 'Studio URL',
37+
description: 'Title for studio url section in info modal',
3138
},
3239
webUrlTitle: {
3340
id: 'course-authoring.files-and-uploads.file-info.webUrl.title',
3441
defaultMessage: 'Web URL',
42+
description: 'Title for web url section in info modal',
3543
},
3644
lockFileTitle: {
3745
id: 'course-authoring.files-and-uploads.file-info.lockFile.title',
3846
defaultMessage: 'Lock file',
39-
},
40-
lockFileTooltipContent: {
41-
id: 'course-authoring.files-and-uploads.file-info.lockFile.tooltip.content',
42-
defaultMessage: `By default, anyone can access a file you upload if
43-
they know the web URL, even if they are not enrolled in your course.
44-
You can prevent outside access to a file by locking the file. When
45-
you lock a file, the web URL only allows learners who are enrolled
46-
in your course and signed in to access the file.`,
47+
description: 'Label for lock file checkbox in info modal',
4748
},
4849
activeCheckboxLabel: {
4950
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.activeCheckbox.label',
5051
defaultMessage: 'Active',
52+
description: 'Label for active checkbox in filter section of sort and filter modal',
5153
},
5254
inactiveCheckboxLabel: {
5355
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.inactiveCheckbox.label',
5456
defaultMessage: 'Inactive',
57+
description: 'Label for inactive checkbox in filter section of sort and filter modal',
5558
},
5659
lockedCheckboxLabel: {
5760
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.lockedCheckbox.label',
5861
defaultMessage: 'Locked',
62+
description: 'Label for locked checkbox in filter section of sort and filter modal',
5963
},
6064
publicCheckboxLabel: {
6165
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.publicCheckbox.label',
6266
defaultMessage: 'Public',
67+
description: 'Label for public checkbox in filter section of sort and filter modal',
6368
},
6469
codeCheckboxLabel: {
6570
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.codeCheckbox.label',
6671
defaultMessage: 'Code',
72+
description: 'Label for code checkbox in filter section of sort and filter modal',
6773
},
6874
imageCheckboxLabel: {
6975
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.imageCheckbox.label',
7076
defaultMessage: 'Images',
77+
description: 'Label for images checkbox in filter section of sort and filter modal',
7178
},
7279
documentCheckboxLabel: {
7380
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.documentCheckbox.label',
7481
defaultMessage: 'Documents',
82+
description: 'Label for documents checkbox in filter section of sort and filter modal',
7583
},
7684
audioCheckboxLabel: {
7785
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.audioCheckbox.label',
7886
defaultMessage: 'Audio',
87+
description: 'Label for audio checkbox in filter section of sort and filter modal',
7988
},
8089
otherCheckboxLabel: {
8190
id: 'course-authoring.files-and-videos.sort-and-filter.modal.filter.otherCheckbox.label',
8291
defaultMessage: 'Other',
92+
description: 'Label for other checkbox in filter section of sort and filter modal',
8393
},
8494
overwriteConfirmMessage: {
8595
id: 'course-authoring.files-and-videos.overwrite.modal.confirmation-message',
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
4+
import { getConfig } from '@edx/frontend-platform';
5+
import {
6+
ActionRow,
7+
AlertModal,
8+
Button,
9+
Collapsible,
10+
Hyperlink,
11+
Truncate,
12+
} from '@openedx/paragon';
13+
14+
import messages from './messages';
15+
16+
const DeleteConfirmationModal = ({
17+
isDeleteConfirmationOpen,
18+
closeDeleteConfirmation,
19+
handleBulkDelete,
20+
selectedRows,
21+
fileType,
22+
// injected
23+
intl,
24+
}) => {
25+
const firstSelectedRow = selectedRows[0]?.original;
26+
let activeContentRows = [];
27+
if (Array.isArray(selectedRows)) {
28+
activeContentRows = selectedRows.filter(row => row.original.activeStatus === 'active');
29+
}
30+
const isDeletingCourseContent = activeContentRows.length > 0;
31+
32+
const deletedCourseContent = activeContentRows.map(({ original }) => (
33+
<li style={{ listStyle: 'None' }} key={original.id}>
34+
<Collapsible
35+
styling="basic"
36+
title={(
37+
<h3 className="h5 m-n2">
38+
<Truncate lines={1}>
39+
{original.displayName}
40+
</Truncate>
41+
</h3>
42+
)}
43+
data-testid={`collapsible-${original.id}`}
44+
>
45+
<ul className="px-2 py-0">
46+
{original.usageLocations.map(location => (
47+
<li key={`usage-location-${location.displayLocation}`} style={{ listStyle: 'square' }}>
48+
<Hyperlink destination={`${getConfig().STUDIO_BASE_URL}${location.url}`} target="_blank">
49+
{location.displayLocation}
50+
</Hyperlink>
51+
</li>
52+
))}
53+
</ul>
54+
</Collapsible>
55+
</li>
56+
));
57+
58+
return (
59+
<AlertModal
60+
className="small"
61+
title={intl.formatMessage(
62+
messages.deleteConfirmationTitle,
63+
{
64+
fileName: firstSelectedRow?.displayName,
65+
fileNumber: selectedRows.length,
66+
fileType,
67+
},
68+
)}
69+
isOpen={isDeleteConfirmationOpen}
70+
onClose={closeDeleteConfirmation}
71+
footerNode={(
72+
<ActionRow>
73+
<Button variant="tertiary" onClick={closeDeleteConfirmation}>
74+
{intl.formatMessage(messages.cancelButtonLabel)}
75+
</Button>
76+
<Button onClick={handleBulkDelete}>
77+
{intl.formatMessage(messages.deleteFileButtonLabel)}
78+
</Button>
79+
</ActionRow>
80+
)}
81+
>
82+
{intl.formatMessage(
83+
messages.deleteConfirmationMessage,
84+
{
85+
fileName: firstSelectedRow?.displayName,
86+
fileNumber: selectedRows.length,
87+
fileType,
88+
},
89+
)}
90+
{isDeletingCourseContent && (
91+
<div className="mt-3">
92+
{intl.formatMessage(
93+
messages.deleteConfirmationUsageMessage,
94+
{
95+
fileNumber: activeContentRows.length,
96+
fileType,
97+
},
98+
)}
99+
<ul className="p-0">
100+
{deletedCourseContent}
101+
</ul>
102+
</div>
103+
)}
104+
</AlertModal>
105+
);
106+
};
107+
108+
DeleteConfirmationModal.defaultProps = {
109+
selectedRows: [],
110+
};
111+
112+
DeleteConfirmationModal.propTypes = {
113+
selectedRows: PropTypes.arrayOf(PropTypes.shape({
114+
original: PropTypes.shape({
115+
id: PropTypes.string,
116+
displayName: PropTypes.string,
117+
activeStatus: PropTypes.string,
118+
usageLocations: PropTypes.arrayOf(PropTypes.shape({
119+
url: PropTypes.string,
120+
displayLocation: PropTypes.string,
121+
})),
122+
}),
123+
})),
124+
isDeleteConfirmationOpen: PropTypes.bool.isRequired,
125+
closeDeleteConfirmation: PropTypes.func.isRequired,
126+
handleBulkDelete: PropTypes.func.isRequired,
127+
fileType: PropTypes.string.isRequired,
128+
// injected
129+
intl: intlShape.isRequired,
130+
};
131+
132+
export default injectIntl(DeleteConfirmationModal);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
render,
3+
screen,
4+
within,
5+
} from '@testing-library/react';
6+
import { IntlProvider } from '@edx/frontend-platform/i18n';
7+
8+
import DeleteConfirmationModal from './DeleteConfirmationModal';
9+
10+
const defaultProps = {
11+
isDeleteConfirmationOpen: true,
12+
closeDeleteConfirmation: jest.fn(),
13+
handleBulkDelete: jest.fn(),
14+
selectedRows: [{
15+
original: {
16+
displayName: 'test',
17+
activeStatus: 'active',
18+
id: 'file-test',
19+
usageLocations: [{
20+
displayLocation: 'unit', url: 'unit/url',
21+
}],
22+
},
23+
}],
24+
fileType: 'file',
25+
};
26+
27+
const renderComponent = (props) => {
28+
render(
29+
<IntlProvider locale="en">
30+
<DeleteConfirmationModal {...props} />
31+
</IntlProvider>,
32+
);
33+
};
34+
35+
describe('DeleteConfirmationModal', () => {
36+
it('should show file name in title', () => {
37+
renderComponent(defaultProps);
38+
const { displayName } = defaultProps.selectedRows[0].original;
39+
40+
expect(screen.getByText(`Delete ${displayName}`)).toBeInTheDocument();
41+
});
42+
43+
it('should show number of files in title', () => {
44+
const props = {
45+
...defaultProps,
46+
selectedRows: [
47+
...defaultProps.selectedRows,
48+
{ original: { displayName: 'test 2', activeStatus: 'inactive' } },
49+
],
50+
};
51+
52+
renderComponent(props);
53+
const numberOfFiles = props.selectedRows.length;
54+
55+
expect(screen.getByText(`Delete ${numberOfFiles} ${defaultProps.fileType}s`)).toBeInTheDocument();
56+
});
57+
58+
it('should not show delete confirmation usage list', () => {
59+
const props = {
60+
...defaultProps,
61+
selectedRows: [],
62+
};
63+
renderComponent(props);
64+
65+
expect(screen.queryByRole('list')).toBeNull();
66+
});
67+
68+
it('should show test file in delete confirmation usage list', () => {
69+
renderComponent(defaultProps);
70+
const deleteUsageList = screen.getByRole('list');
71+
72+
expect(within(deleteUsageList).getByTestId('collapsible-file-test')).toBeVisible();
73+
});
74+
75+
it('should not show test file in delete confirmation usage list', () => {
76+
const props = {
77+
...defaultProps,
78+
selectedRows: [
79+
...defaultProps.selectedRows,
80+
{ original: { displayName: 'test 2', activeStatus: 'inactive', id: 'file-test2' } },
81+
],
82+
};
83+
renderComponent(props);
84+
const deleteUsageList = screen.getByRole('list');
85+
86+
expect(within(deleteUsageList).queryByTestId('collapsible-file-test2')).toBeNull();
87+
});
88+
});

0 commit comments

Comments
 (0)