This commit is contained in:
Markos Gogoulos
2026-04-20 18:43:35 +03:00
parent 690ffd9409
commit c6f713a885
7 changed files with 106 additions and 25 deletions
+1 -1
View File
@@ -1 +1 @@
VERSION = "8.998"
VERSION = "8.999"
+1 -12
View File
@@ -305,21 +305,10 @@ class MediaPublishForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
state = cleaned_data.get("state")
shared = cleaned_data.get("shared")
if self.was_shared and not shared and not cleaned_data.get('confirm_state'):
if state == 'private':
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.")
self.add_error('confirm_state', "I understand that unchecking Shared will remove all existing sharing for this media.")
return cleaned_data
+10 -1
View File
@@ -506,7 +506,16 @@ class MediaBulkUserActions(APIView):
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()
for m in media:
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 {
display: flex;
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 { translateString } from '../utils/helpers/';
@@ -32,14 +32,23 @@ export const BulkActionPublishStateModal: React.FC<BulkActionPublishStateModalPr
const defaultState = availableStates[0].value;
const [selectedState, setSelectedState] = useState(defaultState);
const [shared, setShared] = useState<boolean | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const sharedRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!isOpen) {
setSelectedState(defaultState);
setShared(null);
}
}, [isOpen]);
useEffect(() => {
if (sharedRef.current) {
sharedRef.current.indeterminate = shared === null;
}
}, [shared]);
const handleSubmit = async () => {
if (!selectedState) {
onError(translateString('Please select a publish state'));
@@ -49,17 +58,22 @@ export const BulkActionPublishStateModal: React.FC<BulkActionPublishStateModalPr
setIsProcessing(true);
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', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify({
action: 'set_state',
media_ids: selectedMediaIds,
state: selectedState,
}),
body: JSON.stringify(body),
});
if (!response.ok) {
@@ -79,9 +93,12 @@ export const BulkActionPublishStateModal: React.FC<BulkActionPublishStateModalPr
if (!isOpen) return null;
// Note: We don't check hasStateChanged because the modal doesn't know the actual
// current state of the selected media. Users should be able to set any state.
// If the state is already the same, the backend will handle it gracefully.
const sharedNote =
shared === null
? 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 (
<div className="publish-state-modal-overlay">
@@ -109,6 +126,22 @@ export const BulkActionPublishStateModal: React.FC<BulkActionPublishStateModalPr
))}
</select>
</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 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