mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-04-30 10:26:13 -04:00
al
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
VERSION = "8.99914"
|
||||
VERSION = "8.99917"
|
||||
|
||||
@@ -1,100 +1,203 @@
|
||||
@import '../../css/config/index.scss';
|
||||
|
||||
@keyframes bulk-menu-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bulk-actions-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.bulk-actions-select {
|
||||
width: auto;
|
||||
max-width: 220px;
|
||||
.bulk-actions-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 36px;
|
||||
padding: 0 28px 0 10px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
background-color: #e8e8e8;
|
||||
border-color: #ccc;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
|
||||
background-color: #e4e4e4;
|
||||
border-color: #bbb;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--default-theme-color, #009933);
|
||||
box-shadow: 0 0 0 3px rgba(0, 153, 51, 0.25);
|
||||
box-shadow: 0 0 0 3px rgba(0, 153, 51, 0.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #dcdcdc;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.no-selection {
|
||||
color: #666;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
optgroup {
|
||||
&.is-open {
|
||||
background-color: #e4e4e4;
|
||||
border-color: var(--default-theme-color, #009933);
|
||||
box-shadow: 0 0 0 2px rgba(0, 153, 51, 0.15);
|
||||
}
|
||||
|
||||
.bulk-actions-chevron {
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.is-open .bulk-actions-chevron {
|
||||
transform: rotate(180deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bulk-actions-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 5px);
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
min-width: 230px;
|
||||
max-height: 340px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13), 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
animation: bulk-menu-in 0.15s ease;
|
||||
}
|
||||
|
||||
.bulk-actions-group {
|
||||
padding: 6px 0;
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
.bulk-actions-group-label {
|
||||
padding: 4px 14px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #aaa;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.bulk-actions-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #222;
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease, border-left-color 0.1s ease, color 0.1s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #f0f7f3;
|
||||
border-left-color: var(--default-theme-color, #009933);
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
option {
|
||||
padding: 10px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
color: #333;
|
||||
background-color: white;
|
||||
&:active:not(:disabled) {
|
||||
background-color: #dff0e7;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
color: #000;
|
||||
}
|
||||
&:disabled,
|
||||
&.is-disabled {
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.dark_theme & {
|
||||
.bulk-actions-select {
|
||||
color: #fff;
|
||||
.bulk-actions-trigger {
|
||||
color: #e0e0e0;
|
||||
background-color: #3a3a3a;
|
||||
border-color: #555;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
border-color: #505050;
|
||||
|
||||
&:hover {
|
||||
background-color: #454545;
|
||||
background-color: #444;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--default-theme-color, #009933);
|
||||
box-shadow: 0 0 0 3px rgba(0, 153, 51, 0.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
&.no-selection {
|
||||
color: #aaa;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
optgroup {
|
||||
&.is-open {
|
||||
background-color: #444;
|
||||
border-color: var(--default-theme-color, #009933);
|
||||
box-shadow: 0 0 0 2px rgba(0, 153, 51, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.bulk-actions-menu {
|
||||
background: #2c2c2c;
|
||||
border-color: #484848;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.bulk-actions-group + .bulk-actions-group {
|
||||
border-top-color: #383838;
|
||||
}
|
||||
|
||||
.bulk-actions-group-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.bulk-actions-item {
|
||||
color: #ddd;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #1a3325;
|
||||
border-left-color: var(--default-theme-color, #009933);
|
||||
color: #fff;
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #2a2a2a;
|
||||
color: #fff;
|
||||
&:active:not(:disabled) {
|
||||
background-color: #163020;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #777;
|
||||
}
|
||||
&:disabled,
|
||||
&.is-disabled {
|
||||
color: #484848;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import './BulkActionsDropdown.scss';
|
||||
import { translateString } from '../utils/helpers/';
|
||||
import { inEmbeddedApp } from '../utils/helpers/embeddedApp';
|
||||
@@ -23,6 +23,8 @@ interface BulkActionGroup {
|
||||
|
||||
export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ selectedCount, onActionSelect, hasContributorCourses = false }) => {
|
||||
const isLmsMode = inEmbeddedApp();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const BULK_ACTION_GROUPS: BulkActionGroup[] = [
|
||||
{
|
||||
@@ -64,52 +66,88 @@ export const BulkActionsDropdown: React.FC<BulkActionsDropdownProps> = ({ select
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const noSelection = selectedCount === 0;
|
||||
|
||||
|
||||
const allActions = BULK_ACTION_GROUPS.flatMap((g) => g.actions);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = event.target.value;
|
||||
|
||||
if (!value) return;
|
||||
|
||||
const actionDef = allActions.find((a) => a.value === value);
|
||||
if (noSelection && !actionDef?.allowsNoSelection) {
|
||||
event.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
onActionSelect(value);
|
||||
// Reset dropdown after selection
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
const displayText = noSelection
|
||||
? translateString('Bulk Actions')
|
||||
: `${translateString('Bulk Actions')} (${selectedCount} ${translateString('selected')})`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
const keyHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setIsOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
document.addEventListener('keydown', keyHandler);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handler);
|
||||
document.removeEventListener('keydown', keyHandler);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleSelect = (action: BulkAction) => {
|
||||
const isDisabled = (!action.allowsNoSelection && noSelection) || !action.enabled;
|
||||
if (isDisabled) return;
|
||||
onActionSelect(action.value);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bulk-actions-dropdown">
|
||||
<select
|
||||
className={'bulk-actions-select' + (noSelection ? ' no-selection' : '')}
|
||||
onChange={handleChange}
|
||||
value=""
|
||||
aria-label={translateString('Bulk Actions')}
|
||||
<div className="bulk-actions-dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={'bulk-actions-trigger' + (noSelection ? ' no-selection' : '') + (isOpen ? ' is-open' : '')}
|
||||
onClick={() => setIsOpen((o) => !o)}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<option value="" disabled>
|
||||
{displayText}
|
||||
</option>
|
||||
{BULK_ACTION_GROUPS.map((group) => (
|
||||
<optgroup key={group.label} label={group.label}>
|
||||
{group.actions.map((action) => (
|
||||
<option key={action.value} value={action.value} disabled={(!action.allowsNoSelection && noSelection) || !action.enabled}>
|
||||
{action.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
{displayText}
|
||||
<svg
|
||||
className="bulk-actions-chevron"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="bulk-actions-menu" role="listbox">
|
||||
{BULK_ACTION_GROUPS.map((group) => (
|
||||
<div key={group.label} className="bulk-actions-group">
|
||||
<div className="bulk-actions-group-label">{group.label}</div>
|
||||
{group.actions.map((action) => {
|
||||
const isDisabled = (!action.allowsNoSelection && noSelection) || !action.enabled;
|
||||
return (
|
||||
<button
|
||||
key={action.value}
|
||||
type="button"
|
||||
className={'bulk-actions-item' + (isDisabled ? ' is-disabled' : '')}
|
||||
onClick={() => handleSelect(action)}
|
||||
disabled={isDisabled}
|
||||
role="option"
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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
+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
+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
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