Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
6 changes: 3 additions & 3 deletions lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,9 @@
"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 items": "Alle Elemente abwählen",
"Deselect items": "Elemente abwählen",
"Detach": "Trennen",
"Details": "Details",
"Dictionary": "Wörterbuch",
Expand Down Expand Up @@ -1001,7 +1001,7 @@
"Select": "Auswählen",
"Select :title": ":title auswählen",
"Select Across Sites": "Seitenübergreifende Auswahl",
"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
6 changes: 3 additions & 3 deletions lang/de_CH.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,9 @@
"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 items": "Alle Elemente abwählen",
"Deselect items": "Elemente abwählen",
"Detach": "Trennen",
"Details": "Details",
"Dictionary": "Wörterbuch",
Expand Down Expand Up @@ -1001,7 +1001,7 @@
"Select": "Auswählen",
"Select :title": ":title auswählen",
"Select Across Sites": "Seitenübergreifende Auswahl",
"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
7 changes: 4 additions & 3 deletions lang/en/messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,13 @@
'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',
'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 @@ -383,9 +383,9 @@
"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 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 @@ -1001,7 +1001,7 @@
"Select": "Choisir",
"Select :title": "Choisir :title",
"Select Across Sites": "Choisir au sein des sites",
"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
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 @@ -88,12 +88,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:bg-gray-800 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'"
Comment thread
jaygeorge marked this conversation as resolved.
>
<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