From 9d743d4d362375c08511da219688348d181e20b6 Mon Sep 17 00:00:00 2001 From: Jay George Date: Wed, 26 Aug 2026 16:47:27 +0100 Subject: [PATCH 01/23] wip --- lang/en/messages.php | 1 + .../js/components/ui/Listing/BulkActions.vue | 17 +++- .../ui/Listing/BulkActionsFloatingToolbar.vue | 27 +++++- .../js/components/ui/Listing/Listing.vue | 77 +++++++++++++++ .../js/components/ui/Listing/ToggleAll.vue | 65 ++++++++++--- resources/js/tests/components/Listing.test.js | 16 ++++ .../js/tests/util/listing-selections.test.js | 95 +++++++++++++++++++ resources/js/util/listing-selections.js | 58 +++++++++++ 8 files changed, 337 insertions(+), 19 deletions(-) create mode 100644 resources/js/tests/util/listing-selections.test.js create mode 100644 resources/js/util/listing-selections.js diff --git a/lang/en/messages.php b/lang/en/messages.php index 5899880f2c1..2c9e192d821 100644 --- a/lang/en/messages.php +++ b/lang/en/messages.php @@ -237,6 +237,7 @@ '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_matching' => 'Select all :total matching', '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.', diff --git a/resources/js/components/ui/Listing/BulkActions.vue b/resources/js/components/ui/Listing/BulkActions.vue index 561130b35a2..6ecdcc6d1fb 100644 --- a/resources/js/components/ui/Listing/BulkActions.vue +++ b/resources/js/components/ui/Listing/BulkActions.vue @@ -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); @@ -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" /> diff --git a/resources/js/components/ui/Listing/BulkActionsFloatingToolbar.vue b/resources/js/components/ui/Listing/BulkActionsFloatingToolbar.vue index 17e3d8f2f0a..5f3d7ac9e59 100644 --- a/resources/js/components/ui/Listing/BulkActionsFloatingToolbar.vue +++ b/resources/js/components/ui/Listing/BulkActionsFloatingToolbar.vue @@ -7,7 +7,7 @@ * Delete uses the backspace icon and is triggered by Delete/Backspace only. */ import { Motion } from 'motion-v'; -import { computed, onMounted, onUnmounted } from 'vue'; +import { computed, onMounted, onUnmounted, toValue } from 'vue'; import { Button, ButtonGroup, Icon } from '@ui'; // ——— Keyboard shortcut constants ——— @@ -23,9 +23,15 @@ const props = defineProps({ visible: { type: Boolean, default: false }, selections: { type: Array, default: () => [] }, clearSelections: { type: Function, default: null }, + canSelectAllMatching: { type: [Boolean, Object], default: false }, + selectAllMatching: { type: Function, default: null }, + selectingAllMatching: { type: [Boolean, Object], default: false }, + matchingTotal: { type: Number, default: 0 }, }); const hasSelections = computed(() => (props.selections?.length ?? 0) > 0); +const showSelectAllMatching = computed(() => !!toValue(props.canSelectAllMatching)); +const isSelectingAllMatching = computed(() => !!toValue(props.selectingAllMatching)); /** True if this action is the built-in delete (by handle or trash icon). */ function isDeleteAction(action) { @@ -117,7 +123,22 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown, true)); :animate="{ y: 0, opacity: 1 }" :transition="{ duration: 0.2, ease: 'easeInOut' }" > -
+
+
+ +