Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9d743d4
wip
jaygeorge Aug 26, 2026
66fac87
Tweak selection messaging
jaygeorge Aug 26, 2026
d41d2bb
Make floating toolbar opaque so extra message is visible
jaygeorge Aug 26, 2026
faf22ee
Tighten up text
jaygeorge Aug 26, 2026
3cdaf4c
Remove weird animation. It was morphing the toolbar when the deselect…
jaygeorge Aug 26, 2026
97cf020
Add a keyboard shortcut
jaygeorge Aug 26, 2026
820bdec
Tweak keyboard shortcut decoration
jaygeorge Aug 26, 2026
3ebd0a6
Fix select-all-matching under the pagination ceiling
jaygeorge Aug 26, 2026
2471a9a
Make blue AA
jaygeorge Aug 26, 2026
df5b6de
Tweak colors
jaygeorge Aug 26, 2026
72818c6
Improve readability
jaygeorge Aug 26, 2026
21905e5
Fix indeterminate checkbox dash for Reka
jaygeorge Aug 26, 2026
831e61b
Keep select-all-matching CTA stable after layout-only changes
jaygeorge Aug 26, 2026
b112fde
Merge branch '6.x' into select-all-including-all-pages
jaygeorge Aug 27, 2026
9ef6ddd
Disable select-all-matching on asset browse listings. I'm not sure it…
jaygeorge Aug 27, 2026
abc4c96
Reset select-all-matching when listing scope changes
jaygeorge Aug 27, 2026
46da85b
Deduplicate IDs when paging select-all-matching results
jaygeorge Aug 27, 2026
27fd51b
Add configurable select-all limit for listing bulk actions
jaygeorge Aug 27, 2026
2547432
Keep select-all-matching when fetched count undershoots total
jaygeorge Aug 27, 2026
fad4bf7
Request select-all-matching IDs more efficiently
jaygeorge Aug 27, 2026
4cce87e
Always reserve A for select-all-matching shortcuts
jaygeorge Aug 27, 2026
94fbc3f
Simplify select-all-matching toolbar prop types
jaygeorge Aug 27, 2026
46a7efa
Use plain axios.isCancel in listing requests
jaygeorge Aug 27, 2026
12eddbb
Update locales
jaygeorge Aug 27, 2026
6ee90a8
Fall back to the default select-all limit for zero or invalid values
jasonvarga Sep 28, 2026
e0b5459
Make toolbar dark background classes mutually exclusive
jasonvarga Sep 28, 2026
c5b059d
Use the actual selected count in the toggle-all description
jasonvarga Sep 28, 2026
8d1b9bf
Restore config stub after each listing test and cover toggle-all count
jasonvarga Sep 28, 2026
106f38d
Merge remote-tracking branch 'origin/6.x' into select-all-including-a…
jasonvarga Sep 28, 2026
0b72c80
Update Turkish translations for renamed selection keys
jasonvarga Sep 28, 2026
2c67420
Unmount listing test wrappers so the delayed bulk actions toolbar can…
jasonvarga Sep 28, 2026
d1951bd
Merge 6.x into select-all-including-all-pages
jackmcdade Oct 14, 2026
4fd92b6
Enforce select-all limits throughout paginated requests
jackmcdade Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions config/cp.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@

'pagination_size_options' => [10, 25, 50, 100],

/*
|--------------------------------------------------------------------------
| Select All Limit
|--------------------------------------------------------------------------
|
| The maximum number of matching results that can be selected across pages
| via "Select all". Raise this if your server can handle larger bulk
| actions, or set it to null for no limit.
|
*/

'select_all_limit' => 1000,

/*
|--------------------------------------------------------------------------
| Links to Documentation
Expand Down
4 changes: 3 additions & 1 deletion lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,11 @@
"Delete|Delete :count items?": "Löschen|:count Elemente löschen?",
"Descending": "Absteigend",
"Description of the image": "Bildbeschreibung",
"Deselect :count item|Deselect all :count items": ":count Element abwählen|Alle :count Elemente abwählen",
"Deselect :count item|Deselect :count items": ":count Element abwählen|:count Elemente abwählen",
"Deselect :title": ":title abwählen",
"Deselect All": "Alle abwählen",
"Deselect all items": "Alle Elemente abwählen",
"Deselect items": "Elemente abwählen",
"Detach": "Trennen",
"Details": "Details",
"Dictionary": "Wörterbuch",
Expand Down Expand Up @@ -1013,6 +1014,7 @@
"Select Across Sites": "Seitenübergreifende Auswahl",
"Select All": "Alle auswählen",
"Select all items": "Alle Elemente auswählen",
"Select items": "Elemente auswählen",
"Select aspect ratio": "Seitenverhältnis wählen",
"Select Collection(s)": "Sammlung(en) auswählen",
"Select date": "Datum auswählen",
Expand Down
4 changes: 3 additions & 1 deletion lang/de_CH.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,11 @@
"Delete|Delete :count items?": "Löschen|:count Elemente löschen?",
"Descending": "Absteigend",
"Description of the image": "Bildbeschreibung",
"Deselect :count item|Deselect all :count items": ":count Element abwählen|Alle :count Elemente abwählen",
"Deselect :count item|Deselect :count items": ":count Element abwählen|:count Elemente abwählen",
"Deselect :title": ":title abwählen",
"Deselect All": "Alle abwählen",
"Deselect all items": "Alle Elemente abwählen",
"Deselect items": "Elemente abwählen",
"Detach": "Trennen",
"Details": "Details",
"Dictionary": "Wörterbuch",
Expand Down Expand Up @@ -1013,6 +1014,7 @@
"Select Across Sites": "Seitenübergreifende Auswahl",
"Select All": "Alle auswählen",
"Select all items": "Alle Elemente auswählen",
"Select items": "Elemente auswählen",
"Select aspect ratio": "Seitenverhältnis wählen",
"Select Collection(s)": "Sammlung(en) auswählen",
"Select date": "Datum auswählen",
Expand Down
8 changes: 5 additions & 3 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,14 @@
'role_intro' => 'Roles are groups of access and action permissions that can be assigned to users and user groups.',
'role_title_instructions' => 'Use a singular noun, such as \'Editor\' or \'Admin\'',
'search_utility_description' => 'Manage and view important information about Statamic\'s search indexes.',
'selections_click_to_deselect_all' => 'All :total items selected. Uncheck to deselect all items.',
'selections_click_to_select_all' => 'No items selected. Check to select all :total items.',
'selections_click_to_deselect_all' => 'All :total items selected. Uncheck to deselect items.',
'selections_click_to_select_all' => 'No items selected. Check to select :total items.',
'selections_item_selected' => ':title is currently selected. Click to deselect.',
'selections_item_unselected' => ':title is not selected. Click to select.',
'selections_limit_reached' => 'Selection limit reached. Cannot select :title',
'selections_select_all' => ':selected of :total items selected. Check to select all items.',
'selections_select_all' => ':selected of :total items selected. Check to select items.',
'selections_select_all_matching' => 'Select :total items across all pages',
'selections_select_all_limit_reached' => 'The results now exceed the selection limit of :limit items. Refresh the listing and try again.',
'session_expiry_dismissed_banner' => 'Your session is about to expire. Click here to extend it and stay signed in.',
'session_expiry_dismissed_login_banner' => 'Your session has expired. Click here to log back in.',
'session_expiry_enter_password' => 'Enter your password to continue.',
Expand Down
6 changes: 3 additions & 3 deletions lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,10 @@
"Delete|Delete :count items?": "Supprimer|Supprimer :count éléments ?",
"Descending": "Décroissant",
"Description of the image": "Description de l'image",
"Deselect :count item|Deselect all :count items": "Désélectionner :count élément|Désélectionner les :count éléments",
"Deselect :count item|Deselect :count items": "Désélectionner :count élément|Désélectionner :count éléments",
"Deselect :title": "Désélectionner :title",
"Deselect All": "Tout désélectionner",
"Deselect all items": "Désélectionner tous les éléments",
"Deselect items": "Désélectionner les éléments",
"Detach": "Détacher",
"Details": "Détails",
"Dictionary": "Dictionnaire",
Expand Down Expand Up @@ -1012,7 +1012,7 @@
"Select :title": "Choisir :title",
"Select Across Sites": "Choisir au sein des sites",
"Select All": "Tout sélectionner",
"Select all items": "Choisir tous les éléments",
"Select items": "Choisir les éléments",
"Select aspect ratio": "Sélectionner le ratio d'aspect",
"Select Collection(s)": "Choisir collection(s)",
"Select date": "Choisir la date",
Expand Down
2 changes: 1 addition & 1 deletion lang/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@
"Delete|Delete :count items?": "Slette|Slette :count oppføringer?",
"Descending": "Synkende",
"Description of the image": "Beskrivelse av bildet",
"Deselect :count item|Deselect all :count items": "Fjern markering på :count element|Fjern markering på :count elementer",
"Deselect :count item|Deselect :count items": "Fjern markering på :count element|Fjern markering på :count elementer",
"Deselect option": "Fjern valg",
"Detach": "Koble fra",
"Dictionary": "Ordbok",
Expand Down
6 changes: 3 additions & 3 deletions lang/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -370,9 +370,9 @@
"Delete|Delete :count items?": "Verwijder|Verwijder :count items?",
"Descending": "Aflopend",
"Description of the image": "Omschrijving van de afbeelding",
"Deselect :count item|Deselect all :count items": "Deselecteer :count item|Deselecteer alle :count items",
"Deselect :count item|Deselect :count items": "Deselecteer :count item|Deselecteer :count items",
"Deselect :title": "Deselecteer :title",
"Deselect all items": "Deselecteer alle items",
"Deselect items": "Deselecteer items",
"Deselect option": "Deselecteer optie",
"Detach": "Losmaken",
"Details": "Details",
Expand Down Expand Up @@ -958,7 +958,7 @@
"Select": "Selecteer",
"Select :title": "Selecteer :title",
"Select Across Sites": "Selecteer over sites heen",
"Select all items": "Selecteer alle items",
"Select items": "Selecteer items",
"Select aspect ratio": "Selecteer beeldverhouding",
"Select Collection(s)": "Selecteer collectie(s)",
"Select date": "Selecteer datum",
Expand Down
6 changes: 3 additions & 3 deletions lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,9 @@
"Delete|Delete :count items?": "Удалить|Удалить :count элемента?|Удалить :count элементов?",
"Descending": "По убыванию",
"Description of the image": "Описание изображения",
"Deselect :count item|Deselect all :count items": "Снять выделение с :count элемента|Снять выделение со всех :count элементов",
"Deselect :count item|Deselect :count items": "Снять выделение с :count элемента|Снять выделение с :count элементов",
"Deselect :title": "Снять выделение с :title",
"Deselect all items": "Снять выделение со всех элементов",
"Deselect items": "Снять выделение с элементов",
"Deselect option": "Отменить выбор",
"Detach": "Отсоединить",
"Details": "Детали",
Expand Down Expand Up @@ -1018,7 +1018,7 @@
"Select": "Выбор",
"Select :title": "Выбрать :title",
"Select Across Sites": "Выбрать по всем сайтам",
"Select all items": "Выбрать все элементы",
"Select items": "Выбрать элементы",
"Select asset container": "Выбрать контейнер ресурсов",
"Select Collection(s)": "Выбрать коллекции",
"Select date": "Выбрать дату",
Expand Down
6 changes: 3 additions & 3 deletions lang/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,10 @@
"Delete|Delete :count items?": "Sil|:count öğeyi sil?",
"Descending": "Azalan",
"Description of the image": "Görüntünün açıklaması",
"Deselect :count item|Deselect all :count items": ":count öğenin seçimini kaldır|:count öğenin tümünün seçimini kaldır",
"Deselect :count item|Deselect :count items": ":count öğenin seçimini kaldır|:count öğenin seçimini kaldır",
"Deselect :title": ":title seçimini kaldır",
"Deselect All": "Tüm seçimi kaldır",
"Deselect all items": "Tüm öğelerin seçimini kaldır",
"Deselect items": "Öğelerin seçimini kaldır",
"Detach": "Ayrılmak",
"Details": "Ayrıntılar",
"Dictionary": "Sözlük",
Expand Down Expand Up @@ -1011,7 +1011,7 @@
"Select :title": ":title öğesini seç",
"Select Across Sites": "Siteler arasında seç",
"Select All": "Tümünü seç",
"Select all items": "Tüm öğeleri seç",
"Select items": "Öğeleri seç",
"Select aspect ratio": "En boy oranı seçin",
"Select Collection(s)": "Koleksiyon(ları) seç",
"Select date": "Tarih seçin",
Expand Down
1 change: 1 addition & 0 deletions resources/js/components/assets/Browser/Browser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
:action-url="actionUrl"
:action-context="actionContext"
:allow-bulk-actions="allowBulkActions"
:allow-select-all-matching="false"
:selections="selectedAssets"
:max-selections="maxFiles"
:preferences-prefix="preferencesPrefix"
Expand Down
9 changes: 4 additions & 5 deletions resources/js/components/ui/Checkbox/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,11 @@ const containerClasses = computed(() => {
const conditionalProps = computed(() => {
const props_obj = {};

if (props.modelValue !== null) {
props_obj.modelValue = props.modelValue;
}

// Reka CheckboxRoot uses modelValue === 'indeterminate' (no separate indeterminate prop).
if (props.indeterminate) {
props_obj.indeterminate = true;
props_obj.modelValue = 'indeterminate';
} else if (props.modelValue !== null) {
props_obj.modelValue = props.modelValue;
Comment thread
jaygeorge marked this conversation as resolved.
}

// Only add aria-describedby if description exists AND it's not a solo checkbox
Expand Down
17 changes: 15 additions & 2 deletions resources/js/components/ui/Listing/BulkActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
import { Motion } from 'motion-v';
import { injectListingContext } from '../Listing/Listing.vue';
import { computed, ref, watch } from 'vue';
import { Button, ButtonGroup } from '@ui';
import BulkActions from '@/components/actions/BulkActions.vue';
import BulkActionsFloatingToolbar from './BulkActionsFloatingToolbar.vue';

const { actionUrl, actionContext, selections, refresh, clearSelections } = injectListingContext();
const {
actionUrl,
actionContext,
selections,
refresh,
clearSelections,
canSelectAllMatching,
selectAllMatching,
selectingAllMatching,
meta,
} = injectListingContext();
const busy = ref(false);
const hasSelections = computed(() => selections.value.length > 0);
const visible = ref(false);
Expand Down Expand Up @@ -59,6 +68,10 @@ function actionFailed(response) {
:visible="visible"
:selections="selections"
:clear-selections="clearSelections"
:can-select-all-matching="canSelectAllMatching"
:select-all-matching="selectAllMatching"
:selecting-all-matching="selectingAllMatching"
:matching-total="meta?.total ?? 0"
/>
</BulkActions>
</template>
56 changes: 51 additions & 5 deletions resources/js/components/ui/Listing/BulkActionsFloatingToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Renders the floating toolbar when items are selected in a listing, with keyboard shortcuts
* for each action. Shortcuts are derived from the localized action title (first unused letter).
* Delete uses the backspace icon and is triggered by Delete/Backspace only.
* Select-all-matching uses A (always reserved so actions skip it).
*/
import { Motion } from 'motion-v';
import { computed, onMounted, onUnmounted } from 'vue';
Expand All @@ -14,18 +15,29 @@ import { Button, ButtonGroup, Icon } from '@ui';
const DESELECT_SHORTCUT_KEY = 'Escape';
const DESELECT_SHORTCUT_LABEL = 'Esc';
const DELETE_SHORTCUT_KEY = 'Delete';
const SELECT_ALL_MATCHING_SHORTCUT_KEY = 'a';
const SELECT_ALL_MATCHING_SHORTCUT_LABEL = 'A';

const shortcutKeyClasses =
'ms-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded bg-gray-200/75 px-1 font-semibold uppercase text-[0.625rem] text-gray-600 dark:bg-gray-800 dark:text-gray-400';

const selectAllMatchingShortcutKeyClasses =
'ms-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded border border-blue-300/80 px-1 font-semibold uppercase text-[0.625rem] text-blue-600 bg-blue-100/80 dark:border-blue-700 dark:text-blue-400 dark:bg-blue-950';

const props = defineProps({
actions: { type: Array, default: () => [] },
visible: { type: Boolean, default: false },
selections: { type: Array, default: () => [] },
clearSelections: { type: Function, default: null },
canSelectAllMatching: { type: Boolean, default: false },
selectAllMatching: { type: Function, default: null },
selectingAllMatching: { type: Boolean, default: false },
matchingTotal: { type: Number, default: 0 },
});

const hasSelections = computed(() => (props.selections?.length ?? 0) > 0);
const showSelectAllMatching = computed(() => !!props.canSelectAllMatching);
const isSelectingAllMatching = computed(() => !!props.selectingAllMatching);

/** True if this action is the built-in delete (by handle or trash icon). */
function isDeleteAction(action) {
Expand All @@ -47,6 +59,9 @@ function findShortcutKey(action, used) {

const actionsWithShortcuts = computed(() => {
const used = new Set();
// Always reserve A so action shortcuts stay stable whether the select-all row is visible.
used.add(SELECT_ALL_MATCHING_SHORTCUT_KEY);

return (props.actions || []).map((action) => {
// Delete always shows the backspace icon and is triggered by Delete/Backspace only; no letter.
if (isDeleteAction(action)) {
Expand Down Expand Up @@ -95,6 +110,17 @@ function onKeydown(event) {
if (event.metaKey || event.ctrlKey || event.altKey) return;
const key = event.key?.length === 1 ? event.key.toLowerCase() : null;
if (!key) return;

if (
key === SELECT_ALL_MATCHING_SHORTCUT_KEY &&
showSelectAllMatching.value &&
!isSelectingAllMatching.value
) {
props.selectAllMatching?.();
event.preventDefault();
return;
}

const action = actionsWithShortcuts.value.find((a) => a.shortcutKey === key);
if (action?.run) {
action.run();
Expand All @@ -110,20 +136,40 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown, true));
<template>
<Motion
v-if="visible"
layout
data-floating-toolbar
class="pointer-events-none sticky inset-x-0 bottom-1 sm:bottom-6 z-(--z-index-above) flex w-full max-w-[95vw] mx-auto justify-center"
:initial="{ y: 100, opacity: 0 }"
:animate="{ y: 0, opacity: 1 }"
:transition="{ duration: 0.2, ease: 'easeInOut' }"
>
<div class="pointer-events-auto space-y-3 rounded-xl border border-gray-300/60 dark:border-gray-700 p-1 bg-gray-200/55 shadow-[0_1px_16px_-2px_rgba(63,63,71,0.2)] dark:bg-gray-800 dark:shadow-[0_10px_15px_rgba(0,0,0,.5)] dark:inset-shadow-2xs dark:inset-shadow-white/10">
<div
class="pointer-events-auto space-y-1 rounded-xl border border-gray-300/60 dark:border-gray-700 p-1 shadow-[0_1px_16px_-2px_rgba(63,63,71,0.2)] dark:shadow-[0_10px_15px_rgba(0,0,0,.5)] dark:inset-shadow-2xs dark:inset-shadow-white/10"
:class="showSelectAllMatching ? 'bg-white dark:bg-gray-900' : 'bg-gray-200/55 dark:bg-gray-800'"
>
<div
v-if="showSelectAllMatching"
class="flex justify-center px-2"
>
<Button
class="text-blue-600 dark:text-blue-500 tracking-tight"
variant="ghost"
size="sm"
:loading="isSelectingAllMatching"
:disabled="isSelectingAllMatching"
@click="selectAllMatching?.()"
>
{{ __('messages.selections_select_all_matching', { total: matchingTotal }) }}
<span :class="selectAllMatchingShortcutKeyClasses">
{{ SELECT_ALL_MATCHING_SHORTCUT_LABEL }}
</span>
</Button>
</div>
<ButtonGroup overflow="gap" justify="center">
<Button
class="text-blue-500!"
class="text-blue-600! dark:text-blue-500!"
@click="clearSelections?.()"
>
{{ __n(`Deselect :count item|Deselect all :count items`, selections.length) }}
{{ __n(`Deselect :count item|Deselect :count items`, selections.length) }}
Comment thread
jaygeorge marked this conversation as resolved.
<span :class="[shortcutKeyClasses, 'text-blue-600! bg-blue-100/80! dark:text-blue-400! dark:bg-blue-950!']">
{{ DESELECT_SHORTCUT_LABEL }}
</span>
Expand All @@ -135,7 +181,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown, true));
@click="action.run"
>
{{ __(action.title) }}
<!-- Delete always shows backspace icon; other actions show their shortcut letter. -->
<!-- Delete always shows the backspace icon; other actions show their shortcut letter. -->
<span
:class="[
shortcutKeyClasses,
Expand Down
Loading
Loading