mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-05-06 12:37:23 -04:00
all
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
VERSION = "8.998"
|
VERSION = "8.999"
|
||||||
|
|||||||
+1
-12
@@ -305,21 +305,10 @@ class MediaPublishForm(forms.ModelForm):
|
|||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
state = cleaned_data.get("state")
|
|
||||||
shared = cleaned_data.get("shared")
|
shared = cleaned_data.get("shared")
|
||||||
|
|
||||||
if self.was_shared and not shared and not cleaned_data.get('confirm_state'):
|
if self.was_shared and not shared and not cleaned_data.get('confirm_state'):
|
||||||
if state == 'private':
|
self.add_error('confirm_state', "I understand that unchecking Shared will remove all existing sharing for this media.")
|
||||||
error_parts = []
|
|
||||||
rbac_cat_titles = list(self.instance.category.filter(is_rbac_category=True).values_list('title', flat=True))
|
|
||||||
if rbac_cat_titles:
|
|
||||||
error_parts.append(f"shared with users that have access to categories: {', '.join(rbac_cat_titles)}")
|
|
||||||
if self.instance.permissions.exists():
|
|
||||||
error_parts.append("shared by me with other users (visible in 'Shared by me' page)")
|
|
||||||
detail = f" Currently this media is {' and '.join(error_parts)}." if error_parts else ""
|
|
||||||
self.add_error('confirm_state', f"I understand that this will remove all sharing.{detail}")
|
|
||||||
else:
|
|
||||||
self.add_error('confirm_state', "I understand that unchecking Shared will affect existing sharing settings.")
|
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -506,7 +506,16 @@ class MediaBulkUserActions(APIView):
|
|||||||
|
|
||||||
m.save(update_fields=["state", "listable"])
|
m.save(update_fields=["state", "listable"])
|
||||||
|
|
||||||
if state == "private":
|
shared = request.data.get('shared', None)
|
||||||
|
|
||||||
|
if shared is True:
|
||||||
|
for m in media:
|
||||||
|
MediaPermission.objects.get_or_create(
|
||||||
|
media=m,
|
||||||
|
user=request.user,
|
||||||
|
defaults={'owner_user': request.user, 'permission': 'owner'},
|
||||||
|
)
|
||||||
|
elif shared is False or (shared is None and state == 'private'):
|
||||||
MediaPermission.objects.filter(media__in=media).delete()
|
MediaPermission.objects.filter(media__in=media).delete()
|
||||||
for m in media:
|
for m in media:
|
||||||
rbac_cats = m.category.filter(is_rbac_category=True)
|
rbac_cats = m.category.filter(is_rbac_category=True)
|
||||||
|
|||||||
@@ -159,6 +159,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shared-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark_theme & {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-note {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
|
||||||
|
&--warn {
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark_theme & {
|
||||||
|
color: #aaa;
|
||||||
|
|
||||||
|
&--warn {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.publish-state-modal-footer {
|
.publish-state-modal-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import './BulkActionPublishStateModal.scss';
|
import './BulkActionPublishStateModal.scss';
|
||||||
import { translateString } from '../utils/helpers/';
|
import { translateString } from '../utils/helpers/';
|
||||||
|
|
||||||
@@ -32,14 +32,23 @@ export const BulkActionPublishStateModal: React.FC<BulkActionPublishStateModalPr
|
|||||||
const defaultState = availableStates[0].value;
|
const defaultState = availableStates[0].value;
|
||||||
|
|
||||||
const [selectedState, setSelectedState] = useState(defaultState);
|
const [selectedState, setSelectedState] = useState(defaultState);
|
||||||
|
const [shared, setShared] = useState<boolean | null>(null);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const sharedRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setSelectedState(defaultState);
|
setSelectedState(defaultState);
|
||||||
|
setShared(null);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sharedRef.current) {
|
||||||
|
sharedRef.current.indeterminate = shared === null;
|
||||||
|
}
|
||||||
|
}, [shared]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!selectedState) {
|
if (!selectedState) {
|
||||||
onError(translateString('Please select a publish state'));
|
onError(translateString('Please select a publish state'));
|
||||||
@@ -49,17 +58,22 @@ export const BulkActionPublishStateModal: React.FC<BulkActionPublishStateModalPr
|
|||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
action: 'set_state',
|
||||||
|
media_ids: selectedMediaIds,
|
||||||
|
state: selectedState,
|
||||||
|
};
|
||||||
|
if (shared !== null) {
|
||||||
|
body.shared = shared;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/v1/media/user/bulk_actions', {
|
const response = await fetch('/api/v1/media/user/bulk_actions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRFToken': csrfToken,
|
'X-CSRFToken': csrfToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(body),
|
||||||
action: 'set_state',
|
|
||||||
media_ids: selectedMediaIds,
|
|
||||||
state: selectedState,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -79,9 +93,12 @@ export const BulkActionPublishStateModal: React.FC<BulkActionPublishStateModalPr
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
// Note: We don't check hasStateChanged because the modal doesn't know the actual
|
const sharedNote =
|
||||||
// current state of the selected media. Users should be able to set any state.
|
shared === null
|
||||||
// If the state is already the same, the backend will handle it gracefully.
|
? translateString('Sharing status will not be changed.')
|
||||||
|
: shared
|
||||||
|
? translateString('Selected media will be marked as shared.')
|
||||||
|
: translateString('Sharing will be removed from all selected media.');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="publish-state-modal-overlay">
|
<div className="publish-state-modal-overlay">
|
||||||
@@ -109,6 +126,22 @@ export const BulkActionPublishStateModal: React.FC<BulkActionPublishStateModalPr
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="shared-selector">
|
||||||
|
<label className="shared-selector-label">
|
||||||
|
<input
|
||||||
|
ref={sharedRef}
|
||||||
|
type="checkbox"
|
||||||
|
checked={shared === true}
|
||||||
|
onChange={(e) => setShared(e.target.checked)}
|
||||||
|
disabled={isProcessing}
|
||||||
|
/>
|
||||||
|
{translateString('Shared')}
|
||||||
|
</label>
|
||||||
|
<p className={`shared-selector-note${shared === false ? ' shared-selector-note--warn' : ''}`}>
|
||||||
|
{sharedNote}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="publish-state-modal-footer">
|
<div className="publish-state-modal-footer">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user