This commit is contained in:
Markos Gogoulos
2026-04-26 17:17:02 +03:00
parent d96620d0da
commit 0548bf800f
23 changed files with 249 additions and 108 deletions
+1 -1
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long