mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-05-05 20:23:26 -04:00
a
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
VERSION = "8.99"
|
VERSION = "8.991"
|
||||||
|
|||||||
+65
-3
@@ -357,6 +357,7 @@ class MediaBulkUserActions(APIView):
|
|||||||
"remove_from_category",
|
"remove_from_category",
|
||||||
"add_tags",
|
"add_tags",
|
||||||
"remove_tags",
|
"remove_tags",
|
||||||
|
"course_cleanup",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
'playlist_ids': openapi.Schema(
|
'playlist_ids': openapi.Schema(
|
||||||
@@ -404,12 +405,15 @@ class MediaBulkUserActions(APIView):
|
|||||||
media_ids = request.data.get('media_ids', [])
|
media_ids = request.data.get('media_ids', [])
|
||||||
action = request.data.get('action')
|
action = request.data.get('action')
|
||||||
|
|
||||||
if not media_ids:
|
|
||||||
return Response({"detail": "media_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
if not action:
|
if not action:
|
||||||
return Response({"detail": "action is required"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": "action is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if action == "course_cleanup":
|
||||||
|
return self._handle_course_cleanup(request, media_ids)
|
||||||
|
|
||||||
|
if not media_ids:
|
||||||
|
return Response({"detail": "media_ids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
media = Media.objects.filter(user=request.user, friendly_token__in=media_ids)
|
media = Media.objects.filter(user=request.user, friendly_token__in=media_ids)
|
||||||
|
|
||||||
if not media:
|
if not media:
|
||||||
@@ -727,6 +731,64 @@ class MediaBulkUserActions(APIView):
|
|||||||
else:
|
else:
|
||||||
return Response({"detail": f"Unknown action: {action}"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({"detail": f"Unknown action: {action}"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def _handle_course_cleanup(self, request, media_ids):
|
||||||
|
category_uids = request.data.get('category_uids', [])
|
||||||
|
remove_permissions = request.data.get('remove_permissions', False)
|
||||||
|
remove_tags = request.data.get('remove_tags', False)
|
||||||
|
apply_to_all = request.data.get('apply_to_all', False)
|
||||||
|
|
||||||
|
if not category_uids:
|
||||||
|
return Response({"detail": "category_uids is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
categories = Category.objects.filter(uid__in=category_uids)
|
||||||
|
if not categories.exists():
|
||||||
|
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
valid_categories = [cat for cat in categories if request.user.has_contributor_access_to_category(cat)]
|
||||||
|
if not valid_categories:
|
||||||
|
return Response({"detail": "No contributor access to specified categories"}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
has_media = bool(media_ids)
|
||||||
|
selected_media = Media.objects.filter(user=request.user, friendly_token__in=media_ids) if has_media else Media.objects.none()
|
||||||
|
|
||||||
|
for category in valid_categories:
|
||||||
|
# All users who are members of any group linked to this category
|
||||||
|
group_users = User.objects.filter(rbac_groups__in=category.rbac_groups.all()).distinct()
|
||||||
|
|
||||||
|
course_tag = Tag.objects.filter(title=category.title[:100]).first() if remove_tags else None
|
||||||
|
|
||||||
|
all_course_media = Media.objects.filter(category=category)
|
||||||
|
|
||||||
|
if has_media:
|
||||||
|
if remove_permissions:
|
||||||
|
MediaPermission.objects.filter(media__in=selected_media, user__in=group_users).delete()
|
||||||
|
if remove_tags and course_tag:
|
||||||
|
for m in selected_media:
|
||||||
|
m.tags.remove(course_tag)
|
||||||
|
|
||||||
|
if apply_to_all:
|
||||||
|
other_course_media = all_course_media.exclude(friendly_token__in=media_ids)
|
||||||
|
if remove_permissions:
|
||||||
|
MediaPermission.objects.filter(media__in=other_course_media, user__in=group_users).delete()
|
||||||
|
if remove_tags and course_tag:
|
||||||
|
for m in other_course_media:
|
||||||
|
m.tags.remove(course_tag)
|
||||||
|
for m in other_course_media:
|
||||||
|
m.category.remove(category)
|
||||||
|
|
||||||
|
for m in selected_media:
|
||||||
|
m.category.remove(category)
|
||||||
|
else:
|
||||||
|
if remove_permissions:
|
||||||
|
MediaPermission.objects.filter(media__in=all_course_media, user__in=group_users).delete()
|
||||||
|
if remove_tags and course_tag:
|
||||||
|
for m in all_course_media:
|
||||||
|
m.tags.remove(course_tag)
|
||||||
|
for m in all_course_media:
|
||||||
|
m.category.remove(category)
|
||||||
|
|
||||||
|
return Response({"detail": "Course cleanup completed successfully"})
|
||||||
|
|
||||||
|
|
||||||
class MediaDetail(APIView):
|
class MediaDetail(APIView):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
|
|||||||
interface BulkActionsDropdownProps {
|
interface BulkActionsDropdownProps {
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
onActionSelect: (action: string) => void;
|
onActionSelect: (action: string) => void;
|
||||||
|
hasContributorCourses?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BulkAction {
|
interface BulkAction {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
allowsNoSelection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BulkActionGroup {
|
interface BulkActionGroup {
|
||||||
@@ -19,7 +21,7 @@ interface BulkActionGroup {
|
|||||||
actions: BulkAction[];
|
actions: BulkAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect }) => {
|
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect, hasContributorCourses = false }) => {
|
||||||
const isLmsMode = inEmbeddedApp();
|
const isLmsMode = inEmbeddedApp();
|
||||||
|
|
||||||
const BULK_ACTION_GROUPS: BulkActionGroup[] = [
|
const BULK_ACTION_GROUPS: BulkActionGroup[] = [
|
||||||
@@ -56,18 +58,24 @@ export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ select
|
|||||||
{ value: 'change-owner', label: translateString('Change Owner'), enabled: true },
|
{ value: 'change-owner', label: translateString('Change Owner'), enabled: true },
|
||||||
{ value: 'copy-media', label: translateString('Copy Media'), enabled: true },
|
{ value: 'copy-media', label: translateString('Copy Media'), enabled: true },
|
||||||
{ value: 'delete-media', label: translateString('Delete Media'), enabled: true },
|
{ value: 'delete-media', label: translateString('Delete Media'), enabled: true },
|
||||||
|
...(isLmsMode && hasContributorCourses
|
||||||
|
? [{ value: 'course-cleanup', label: translateString('Course Cleanup'), enabled: true, allowsNoSelection: true }]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const noSelection = selectedCount === 0;
|
const noSelection = selectedCount === 0;
|
||||||
|
|
||||||
|
|
||||||
|
const allActions = BULK_ACTION_GROUPS.flatMap((g) => g.actions);
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
|
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
|
||||||
if (noSelection) {
|
const actionDef = allActions.find((a) => a.value === value);
|
||||||
|
if (noSelection && !actionDef?.allowsNoSelection) {
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,7 +103,7 @@ export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ select
|
|||||||
{BULK_ACTION_GROUPS.map((group) => (
|
{BULK_ACTION_GROUPS.map((group) => (
|
||||||
<optgroup key={group.label} label={group.label}>
|
<optgroup key={group.label} label={group.label}>
|
||||||
{group.actions.map((action) => (
|
{group.actions.map((action) => (
|
||||||
<option key={action.value} value={action.value} disabled={noSelection || !action.enabled}>
|
<option key={action.value} value={action.value} disabled={(!action.allowsNoSelection && noSelection) || !action.enabled}>
|
||||||
{action.label}
|
{action.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { BulkActionChangeOwnerModal } from './BulkActionChangeOwnerModal';
|
|||||||
import { BulkActionPublishStateModal } from './BulkActionPublishStateModal';
|
import { BulkActionPublishStateModal } from './BulkActionPublishStateModal';
|
||||||
import { BulkActionCategoryModal } from './BulkActionCategoryModal';
|
import { BulkActionCategoryModal } from './BulkActionCategoryModal';
|
||||||
import { BulkActionTagModal } from './BulkActionTagModal';
|
import { BulkActionTagModal } from './BulkActionTagModal';
|
||||||
|
import { BulkActionCourseCleanupModal } from './BulkActionCourseCleanupModal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders all bulk action modals
|
* Renders all bulk action modals
|
||||||
@@ -58,6 +59,12 @@ export function BulkActionsModals({
|
|||||||
onTagModalSuccess,
|
onTagModalSuccess,
|
||||||
onTagModalError,
|
onTagModalError,
|
||||||
|
|
||||||
|
// Course cleanup modal props
|
||||||
|
showCourseCleanupModal,
|
||||||
|
onCourseCleanupModalCancel,
|
||||||
|
onCourseCleanupModalSuccess,
|
||||||
|
onCourseCleanupModalError,
|
||||||
|
|
||||||
// Common props
|
// Common props
|
||||||
csrfToken,
|
csrfToken,
|
||||||
|
|
||||||
@@ -131,6 +138,15 @@ export function BulkActionsModals({
|
|||||||
csrfToken={csrfToken}
|
csrfToken={csrfToken}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BulkActionCourseCleanupModal
|
||||||
|
isOpen={showCourseCleanupModal}
|
||||||
|
selectedMediaIds={selectedMediaIds}
|
||||||
|
onCancel={onCourseCleanupModalCancel}
|
||||||
|
onSuccess={onCourseCleanupModalSuccess}
|
||||||
|
onError={onCourseCleanupModalError}
|
||||||
|
csrfToken={csrfToken}
|
||||||
|
/>
|
||||||
|
|
||||||
{showNotification && (
|
{showNotification && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -193,6 +209,11 @@ BulkActionsModals.propTypes = {
|
|||||||
onTagModalSuccess: PropTypes.func.isRequired,
|
onTagModalSuccess: PropTypes.func.isRequired,
|
||||||
onTagModalError: PropTypes.func.isRequired,
|
onTagModalError: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
showCourseCleanupModal: PropTypes.bool.isRequired,
|
||||||
|
onCourseCleanupModalCancel: PropTypes.func.isRequired,
|
||||||
|
onCourseCleanupModalSuccess: PropTypes.func.isRequired,
|
||||||
|
onCourseCleanupModalError: PropTypes.func.isRequired,
|
||||||
|
|
||||||
csrfToken: PropTypes.string.isRequired,
|
csrfToken: PropTypes.string.isRequired,
|
||||||
|
|
||||||
showNotification: PropTypes.bool.isRequired,
|
showNotification: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface MediaListWrapperProps {
|
|||||||
onSelectAll?: () => void;
|
onSelectAll?: () => void;
|
||||||
onDeselectAll?: () => void;
|
onDeselectAll?: () => void;
|
||||||
showAddMediaButton?: boolean;
|
showAddMediaButton?: boolean;
|
||||||
|
hasContributorCourses?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
|
export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
|
||||||
@@ -38,6 +39,7 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
|
|||||||
onSelectAll = () => {},
|
onSelectAll = () => {},
|
||||||
onDeselectAll = () => {},
|
onDeselectAll = () => {},
|
||||||
showAddMediaButton = false,
|
showAddMediaButton = false,
|
||||||
|
hasContributorCourses = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [popupContentRef, PopupContent, PopupTrigger] = usePopup() as [any, any, any];
|
const [popupContentRef, PopupContent, PopupTrigger] = usePopup() as [any, any, any];
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ export const MediaListWrapper: React.FC<MediaListWrapperProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="bulk-actions-container">
|
<div className="bulk-actions-container">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} />
|
<BulkActionsDropdown selectedCount={selectedCount} onActionSelect={onBulkAction} hasContributorCourses={hasContributorCourses} />
|
||||||
<SelectAllCheckbox
|
<SelectAllCheckbox
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
selectedCount={selectedCount}
|
selectedCount={selectedCount}
|
||||||
|
|||||||
@@ -463,6 +463,7 @@ class ProfileMediaPage extends Page {
|
|||||||
onSelectAll={this.props.bulkActions.handleSelectAll}
|
onSelectAll={this.props.bulkActions.handleSelectAll}
|
||||||
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
||||||
showAddMediaButton={!isSelectMediaMode && isMediaAuthor}
|
showAddMediaButton={!isSelectMediaMode && isMediaAuthor}
|
||||||
|
hasContributorCourses={this.props.bulkActions.hasContributorCourses}
|
||||||
>
|
>
|
||||||
<ProfileMediaFilters
|
<ProfileMediaFilters
|
||||||
hidden={this.state.hiddenFilters}
|
hidden={this.state.hiddenFilters}
|
||||||
@@ -530,6 +531,10 @@ class ProfileMediaPage extends Page {
|
|||||||
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
||||||
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
||||||
onTagModalError={this.props.bulkActions.handleTagModalError}
|
onTagModalError={this.props.bulkActions.handleTagModalError}
|
||||||
|
showCourseCleanupModal={this.props.bulkActions.showCourseCleanupModal}
|
||||||
|
onCourseCleanupModalCancel={this.props.bulkActions.handleCourseCleanupModalCancel}
|
||||||
|
onCourseCleanupModalSuccess={this.props.bulkActions.handleCourseCleanupModalSuccess}
|
||||||
|
onCourseCleanupModalError={this.props.bulkActions.handleCourseCleanupModalError}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ class ProfileSharedByMePage extends Page {
|
|||||||
onBulkAction={this.props.bulkActions.handleBulkAction}
|
onBulkAction={this.props.bulkActions.handleBulkAction}
|
||||||
onSelectAll={this.props.bulkActions.handleSelectAll}
|
onSelectAll={this.props.bulkActions.handleSelectAll}
|
||||||
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
onDeselectAll={this.props.bulkActions.handleDeselectAll}
|
||||||
|
hasContributorCourses={this.props.bulkActions.hasContributorCourses}
|
||||||
>
|
>
|
||||||
<ProfileMediaFilters
|
<ProfileMediaFilters
|
||||||
hidden={this.state.hiddenFilters}
|
hidden={this.state.hiddenFilters}
|
||||||
@@ -554,6 +555,10 @@ class ProfileSharedByMePage extends Page {
|
|||||||
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
onTagModalCancel={this.props.bulkActions.handleTagModalCancel}
|
||||||
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
onTagModalSuccess={this.props.bulkActions.handleTagModalSuccess}
|
||||||
onTagModalError={this.props.bulkActions.handleTagModalError}
|
onTagModalError={this.props.bulkActions.handleTagModalError}
|
||||||
|
showCourseCleanupModal={this.props.bulkActions.showCourseCleanupModal}
|
||||||
|
onCourseCleanupModalCancel={this.props.bulkActions.handleCourseCleanupModalCancel}
|
||||||
|
onCourseCleanupModalSuccess={this.props.bulkActions.handleCourseCleanupModalSuccess}
|
||||||
|
onCourseCleanupModalError={this.props.bulkActions.handleCourseCleanupModalError}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { translateString } from '../helpers';
|
import { translateString } from '../helpers';
|
||||||
|
import { inEmbeddedApp } from '../helpers/embeddedApp';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom hook for managing bulk actions on media items
|
* Custom hook for managing bulk actions on media items
|
||||||
@@ -22,6 +23,20 @@ export function useBulkActions() {
|
|||||||
const [showPublishStateModal, setShowPublishStateModal] = useState(false);
|
const [showPublishStateModal, setShowPublishStateModal] = useState(false);
|
||||||
const [showCategoryModal, setShowCategoryModal] = useState(false);
|
const [showCategoryModal, setShowCategoryModal] = useState(false);
|
||||||
const [showTagModal, setShowTagModal] = useState(false);
|
const [showTagModal, setShowTagModal] = useState(false);
|
||||||
|
const [showCourseCleanupModal, setShowCourseCleanupModal] = useState(false);
|
||||||
|
const [hasContributorCourses, setHasContributorCourses] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inEmbeddedApp()) return;
|
||||||
|
fetch('/api/v1/categories/contributor?lms_courses_only=true')
|
||||||
|
.then((r) => r.ok ? r.json() : null)
|
||||||
|
.then((data) => {
|
||||||
|
if (!data) return;
|
||||||
|
const courses = data.results || data;
|
||||||
|
setHasContributorCourses(Array.isArray(courses) && courses.length > 0);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Get CSRF token from cookies
|
// Get CSRF token from cookies
|
||||||
const getCsrfToken = () => {
|
const getCsrfToken = () => {
|
||||||
@@ -95,6 +110,11 @@ export function useBulkActions() {
|
|||||||
const handleBulkAction = (action) => {
|
const handleBulkAction = (action) => {
|
||||||
const selectedCount = selectedMedia.size;
|
const selectedCount = selectedMedia.size;
|
||||||
|
|
||||||
|
if (action === 'course-cleanup') {
|
||||||
|
setShowCourseCleanupModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedCount === 0) {
|
if (selectedCount === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -500,6 +520,22 @@ export function useBulkActions() {
|
|||||||
setShowTagModal(false);
|
setShowTagModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Course cleanup modal handlers
|
||||||
|
const handleCourseCleanupModalCancel = () => {
|
||||||
|
setShowCourseCleanupModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCourseCleanupModalSuccess = (message) => {
|
||||||
|
showNotificationMessage(message);
|
||||||
|
clearSelectionAndRefresh();
|
||||||
|
setShowCourseCleanupModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCourseCleanupModalError = (message) => {
|
||||||
|
showNotificationMessage(message, 'error');
|
||||||
|
setShowCourseCleanupModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
selectedMedia,
|
selectedMedia,
|
||||||
@@ -517,6 +553,8 @@ export function useBulkActions() {
|
|||||||
showPublishStateModal,
|
showPublishStateModal,
|
||||||
showCategoryModal,
|
showCategoryModal,
|
||||||
showTagModal,
|
showTagModal,
|
||||||
|
showCourseCleanupModal,
|
||||||
|
hasContributorCourses,
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
handleMediaSelection,
|
handleMediaSelection,
|
||||||
@@ -544,6 +582,9 @@ export function useBulkActions() {
|
|||||||
handleTagModalCancel,
|
handleTagModalCancel,
|
||||||
handleTagModalSuccess,
|
handleTagModalSuccess,
|
||||||
handleTagModalError,
|
handleTagModalError,
|
||||||
|
handleCourseCleanupModalCancel,
|
||||||
|
handleCourseCleanupModalSuccess,
|
||||||
|
handleCourseCleanupModalError,
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
getCsrfToken,
|
getCsrfToken,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user