Skip to content

Commit

Permalink
Disable bulk action buttons if selection is empty (#2233)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek authored Nov 26, 2024
1 parent 8d7a9e2 commit 54c8a40
Show file tree
Hide file tree
Showing 17 changed files with 96 additions and 57 deletions.
13 changes: 0 additions & 13 deletions demos/form-control/calendar.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,6 @@
$control->addAction(['Clear', 'icon' => 'times red'])
->on('click', $control->getJsInstance()->clear());

// TODO "date" type does not support ranges
// $form->addControl('date_range', [
// Form\Control\Calendar::class,
// 'type' => 'date',
// 'options' => ['mode' => 'range'],
// ])->set(date('Y-m-d') . ' to ' . date('Y-m-d', strtotime('+1 week')));
//
// $form->addControl('date_multiple', [
// Form\Control\Calendar::class,
// 'type' => 'date',
// 'options' => ['mode' => 'multiple'],
// ])->set(date('Y-m-d') . ', ' . date('Y-m-d', strtotime('+1 Day')) . ', ' . date('Y-m-d', strtotime('+2 Day')));

$form->onSubmit(static function (Form $form) use ($app) {
$data = [];
foreach ($form->entity->get() as $k => $v) {
Expand Down
3 changes: 2 additions & 1 deletion demos/interactive/modal.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Atk4\Ui\Button;
use Atk4\Ui\Form;
use Atk4\Ui\Header;
use Atk4\Ui\Js\JsBlock;
use Atk4\Ui\Js\JsExpression;
use Atk4\Ui\LoremIpsum;
use Atk4\Ui\Menu;
Expand Down Expand Up @@ -219,7 +220,7 @@
$js[] = $form->jsSuccess('Thank you, ' . $form->entity->get('name') . ' you can go on!');
$js[] = $nextAction->js()->removeClass('disabled');

return $js;
return new JsBlock($js);
});
$p->js(true, $previousAction->js()->removeClass('disabled'));
$p->js(true, $nextAction->js()->addClass('disabled'));
Expand Down
13 changes: 13 additions & 0 deletions js/src/helpers/grid-checkbox.helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,17 @@ export default {
},
});
},

/**
* Invoke callback on checked and indeterminate change.
*/
onMasterCheckboxChange: function (tableSelector, fx) {
const $table = $(tableSelector);
const $masterCheckbox = $table.find('.master.checkbox');

new MutationObserver(() => fx($masterCheckbox.first())).observe($masterCheckbox[0], {
attributes: true,
attributeFilter: ['class'],
});
},
};
11 changes: 11 additions & 0 deletions public/js/atkjs-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ function recomputeMasterCheckbox($table) {
recomputeMasterCheckbox($table);
}
});
},
/**
* Invoke callback on checked and indeterminate change.
*/
onMasterCheckboxChange: function (tableSelector, fx) {
const $table = external_jquery__WEBPACK_IMPORTED_MODULE_5___default()(tableSelector);
const $masterCheckbox = $table.find('.master.checkbox');
new MutationObserver(() => fx($masterCheckbox.first())).observe($masterCheckbox[0], {
attributes: true,
attributeFilter: ['class']
});
}
});

Expand Down
2 changes: 1 addition & 1 deletion public/js/atkjs-ui.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/js/atkjs-ui.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/js/atkjs-ui.min.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/Behat/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ public function elementAttributeShouldContainText(string $selector, string $attr
}

/**
* @Then Element :arg1 should contain class :arg3
* @Then Element :arg1 should contain class :arg2
*/
public function elementShouldContainClass(string $selector, string $class): void
{
Expand All @@ -864,7 +864,7 @@ public function elementShouldContainClass(string $selector, string $class): void
}

/**
* @Then Element :arg1 should not contain class :arg3
* @Then Element :arg1 should not contain class :arg2
*/
public function elementShouldNotContainClass(string $selector, string $class): void
{
Expand Down
4 changes: 4 additions & 0 deletions src/Card.php
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ public function addClickAction(Model\UserAction $action, ?Button $button = null,
}
}

if ($action->enabled === false) {
$button->addClass('disabled');
}

$button->on('click', $cardDeck !== null ? $cardDeck->sharedExecutorsContainer->getExecutor($action) : $action, $defaults);

return $this;
Expand Down
13 changes: 10 additions & 3 deletions src/CardDeck.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,18 @@ public function setModel(Model $model, ?array $fields = null, ?array $extra = nu
// add no record scope action to menu
if ($this->useAction && $this->menu) {
foreach ($this->getModelActions(Model\UserAction::APPLIES_TO_NO_RECORD) as $k => $action) {
$executor = $this->initActionExecutor($action);
$this->menuActions[$k]['button'] = $this->menu->addItem(
$item = $this->menu->addItem(
$this->getExecutorFactory()->createTrigger($action, ExecutorFactory::MENU_ITEM)
);
$this->menuActions[$k]['executor'] = $executor;

if ($action->enabled === false) {
$item->addClass('disabled');
}

$this->menuActions[$k] = [
'button' => $item,
'executor' => $this->initActionExecutor($action),
];
}
}

Expand Down
17 changes: 11 additions & 6 deletions src/Crud.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,18 @@ public function setModel(Model $model, ?array $fields = null): void

if ($this->menu) {
foreach ($this->_getModelActions(Model\UserAction::APPLIES_TO_NO_RECORD) as $k => $action) {
if ($action->enabled) {
$executor = $this->initActionExecutor($action);
$this->menuItems[$k]['item'] = $this->menu->addItem(
$this->getExecutorFactory()->createTrigger($action, ExecutorFactory::MENU_ITEM)
);
$this->menuItems[$k]['executor'] = $executor;
$item = $this->menu->addItem(
$this->getExecutorFactory()->createTrigger($action, ExecutorFactory::MENU_ITEM)
);

if ($action->enabled === false) {
$item->addClass('disabled');
}

$this->menuItems[$k] = [
'item' => $item,
'executor' => $this->initActionExecutor($action),
];
}
$this->setItemsAction();
}
Expand Down
28 changes: 21 additions & 7 deletions src/Grid.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use Atk4\Data\Model;
use Atk4\Ui\Js\Jquery;
use Atk4\Ui\Js\JsCallbackLoadableValue;
use Atk4\Ui\Js\JsExpression;
use Atk4\Ui\Js\JsExpressionable;
use Atk4\Ui\Js\JsFunction;
use Atk4\Ui\Js\JsReload;
use Atk4\Ui\UserAction\ConfirmationExecutor;
use Atk4\Ui\UserAction\ExecutorFactory;
Expand Down Expand Up @@ -517,19 +519,30 @@ private function explodeSelectionValue(string $value): array
return $res;
}

private function setupOnMasterCheckboxChangeDisabled(View $item): void
{
$item->addClass('disabled');

$this->table->js(true, new JsExpression('atk.gridCheckboxHelper.onMasterCheckboxChange([], []);', [
$this->table,
new JsFunction(['$el'], [new JsExpression('$([]).toggleClass(\'disabled\', !$el.hasClass(\'checked\') && !$el.hasClass(\'indeterminate\'))', [$item])]),
]));
}

/**
* Similar to addActionButton but apply to a multiple records selection and display in menu.
* When menu item is clicked, $callback is executed.
*
* @param string|array<mixed>|MenuItem $item
* @param \Closure(Jquery, list<mixed>): JsExpressionable $callback
* @param array<string, string> $args extra URL argument for callback
* @param string|array<mixed>|MenuItem $item
* @param \Closure(Jquery, non-empty-list<mixed>): JsExpressionable $callback
* @param array<string, string> $args extra URL argument for callback
*
* @return View
*/
public function addBulkAction($item, \Closure $callback, $args = [])
{
$menuItem = $this->menu->addItem($item);
$this->setupOnMasterCheckboxChangeDisabled($menuItem);
$menuItem->on('click', static function (Jquery $j, array $ids) use ($callback) {
return $callback($j, $ids);
}, [
Expand All @@ -545,10 +558,10 @@ public function addBulkAction($item, \Closure $callback, $args = [])
* Similar to addModalAction but apply to a multiple records selection and display in menu.
* When menu item is clicked, modal is displayed with the $title and $callback is executed.
*
* @param string|array<mixed>|MenuItem $item
* @param string $title
* @param \Closure(View, list<mixed>): void $callback
* @param array<string, string> $args extra URL argument for callback
* @param string|array<mixed>|MenuItem $item
* @param string $title
* @param \Closure(View, non-empty-list<mixed>): void $callback
* @param array<string, string> $args extra URL argument for callback
*
* @return View
*/
Expand All @@ -562,6 +575,7 @@ public function addModalBulkAction($item, $title, \Closure $callback, $args = []
});

$menuItem = $this->menu->addItem($item);
$this->setupOnMasterCheckboxChangeDisabled($menuItem);
$menuItem->on('click', $modal->jsShow(array_merge([$this->name => $this->selection->jsChecked()], $args)));

return $menuItem;
Expand Down
2 changes: 1 addition & 1 deletion src/Persistence/Ui.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ protected function _typecastSaveField(Field $field, $value): ?string
break;
case 'atk4_money':
$value = parent::_typecastLoadField($field, $value);
$valueDecimals = strlen(preg_replace('~^[^.]$|^.+\.|0+$~s', '', number_format($value, max(0, 11 - (int) log10($value)), '.', '')));
$valueDecimals = strlen(preg_replace('~^[^.]$|^.+\.|0+$~s', '', number_format($value, max(0, 11 - (int) log10(abs($value))), '.', '')));
$value = ($this->currency ? $this->currency . ' ' : '')
. number_format($value, max($this->currencyDecimals, $valueDecimals), $this->decimalSeparator, $this->thousandsSeparator);
$value = str_replace(' ', "\u{00a0}" /* Unicode NBSP */, $value);
Expand Down
8 changes: 3 additions & 5 deletions src/Table/Column/ActionButtons.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ActionButtons extends Table\Column
/** @var array<string, View> Stores all the buttons that have been added. */
public $buttons = [];

/** @var array<string, \Closure<T of Model>(T): bool> Callbacks as defined in UserAction->enabled for evaluating row-specific if an action is enabled. */
/** @var array<string, false|\Closure<T of Model>(T): bool> Callbacks as defined in UserAction->enabled for evaluating row-specific if an action is enabled. */
protected $isEnabledFxs = [];

#[\Override]
Expand Down Expand Up @@ -60,9 +60,7 @@ public function addButton($button, $action = null, string $confirmMsg = '', $isE

$this->assertColumnViewNotInitialized($button);

if ($isEnabled === false) {
$button->addClass('disabled');
} elseif ($isEnabled !== true) {
if ($isEnabled !== true) {
$this->isEnabledFxs[$name] = $isEnabled;
}

Expand Down Expand Up @@ -148,7 +146,7 @@ public function getHtmlTags(Model $row, ?Field $field): array
{
$tags = [];
foreach ($this->isEnabledFxs as $name => $isEnabledFx) {
if (!$isEnabledFx($row)) {
if ($isEnabledFx === false || !$isEnabledFx($row)) {
$tags['_' . $name . '_disabled'] = 'disabled';
}
}
Expand Down
8 changes: 3 additions & 5 deletions src/Table/Column/ActionMenu.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ActionMenu extends Table\Column
/** @var list<View> Menu items collections. */
protected $items = [];

/** @var array<string, \Closure<T of Model>(T): bool> Callbacks as defined in UserAction->enabled for evaluating row-specific if an action is enabled. */
/** @var array<string, false|\Closure<T of Model>(T): bool> Callbacks as defined in UserAction->enabled for evaluating row-specific if an action is enabled. */
protected $isEnabledFxs = [];

/** @var string Dropdown label. */
Expand Down Expand Up @@ -75,9 +75,7 @@ public function addActionMenuItem($item, $action = null, string $confirmMsg = ''

$item->addClass('{$_' . $name . '_disabled} i_' . $name);

if ($isEnabled === false) {
$item->addClass('disabled');
} elseif ($isEnabled !== true) {
if ($isEnabled !== true) {
$this->isEnabledFxs[$name] = $isEnabled;
}

Expand Down Expand Up @@ -146,7 +144,7 @@ public function getHtmlTags(Model $row, ?Field $field): array
{
$tags = [];
foreach ($this->isEnabledFxs as $name => $isEnabledFx) {
if (!$isEnabledFx($row)) {
if ($isEnabledFx === false || !$isEnabledFx($row)) {
$tags['_' . $name . '_disabled'] = 'disabled';
}
}
Expand Down
20 changes: 9 additions & 11 deletions tests-behat/grid.feature
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,29 @@ Feature: Grid
Given I am on "_unit-test/grid-master-checkbox.php"
Then Element "//div.ui.master.checkbox" should not contain class "checked"
Then Element "//div.ui.master.checkbox" should not contain class "indeterminate"
When I press button "Show selected"
Then Toast display should contain text "Selected: #"
Then Element "//div.ui.menu/div.item[text()='Show selected']" should contain class "disabled"
When I click using selector "//tr[1]//div.ui.child.checkbox"
Then Element "//div.ui.master.checkbox" should not contain class "checked"
Then Element "//div.ui.master.checkbox" should contain class "indeterminate"
Then Element "//div.ui.menu/div.item[text()='Show selected']" should not contain class "disabled"
Then I press button "Show selected"
Then Toast display should contain text "Selected: 1#"
When I click using selector "//div.ui.master.checkbox"
Then Element "//div.ui.master.checkbox" should contain class "checked"
Then Element "//div.ui.master.checkbox" should not contain class "indeterminate"
Then Element "//div.ui.menu/div.item[text()='Show selected']" should not contain class "disabled"
Then I press button "Show selected"
Then Toast display should contain text "Selected: 1, 2, 3, 4, 5#"
When I click using selector "//div.ui.master.checkbox"
Then Element "//div.ui.master.checkbox" should not contain class "checked"
Then Element "//div.ui.master.checkbox" should not contain class "indeterminate"
Then I press button "Show selected"
Then Toast display should contain text "Selected: #"
Then Element "//div.ui.menu/div.item[text()='Show selected']" should contain class "disabled"
Then I click paginator page "2"
When I click using selector "//tr[2]//div.ui.child.checkbox"
When I click using selector "//tr[4]//div.ui.child.checkbox"
Then Element "//div.ui.master.checkbox" should not contain class "checked"
Then Element "//div.ui.master.checkbox" should contain class "indeterminate"
Then Element "//div.ui.menu/div.item[text()='Show selected']" should not contain class "disabled"
Then I press button "Show selected"
Then Toast display should contain text "Selected: 7, 9#"

Expand Down Expand Up @@ -124,22 +125,19 @@ Feature: Grid

Scenario: Bulk action
Given I am on "collection/grid.php"
Then I press button "Show selected"
Then Toast display should contain text "Selected: #"
Then Element "//div.ui.menu/div.item[text()='Show selected']" should contain class "disabled"
When I click using selector "//tr[5]//div.ui.checkbox"
When I click using selector "//tr[8]//div.ui.checkbox"
Then Element "//div.ui.menu/div.item[text()='Show selected']" should not contain class "disabled"
Then I press button "Show selected"
Then Toast display should contain text "Selected: 5, 8#"

Scenario: Bulk modal action
Given I am on "collection/grid.php"
Then I press button "Delete selected"
Then Modal is open with text "The selected records will be permanently deleted: #"
Then I press button "Delete"
Then I should see "Success"
Then I click close modal
Then Element "//div.ui.menu/div.item[text()='Delete selected']" should contain class "disabled"
When I click using selector "//tr[5]//div.ui.checkbox"
When I click using selector "//tr[8]//div.ui.checkbox"
Then Element "//div.ui.menu/div.item[text()='Delete selected']" should not contain class "disabled"
Then I press button "Delete selected"
Then Modal is open with text "The selected records will be permanently deleted: 5, 8#"
Then I press button "Delete"
Expand Down
3 changes: 3 additions & 0 deletions tests/PersistenceUiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,12 @@ public static function provideTypecastBidirectionalCases(): iterable
yield [['currencyDecimals' => 1], ['type' => 'atk4_money'], 1.1, $fixSpaceToNbspFx('€ 1.1')];
yield [['currencyDecimals' => 4], ['type' => 'atk4_money'], 1.102, $fixSpaceToNbspFx('€ 1.1020')];
yield [[], ['type' => 'atk4_money'], 1_234_056_789.1, $fixSpaceToNbspFx('€ 1 234 056 789.10')];
yield [[], ['type' => 'atk4_money'], -1_234_056_789.1, $fixSpaceToNbspFx('€ -1 234 056 789.10')];
yield [[], ['type' => 'atk4_money'], 234_056_789.101, $fixSpaceToNbspFx('€ 234 056 789.101')];
yield [[], ['type' => 'atk4_money'], -234_056_789.101, $fixSpaceToNbspFx('€ -234 056 789.101')];
yield [['decimalSeparator' => ','], ['type' => 'atk4_money'], 1.0, $fixSpaceToNbspFx('€ 1,00')];
yield [[], ['type' => 'atk4_money'], 12_345_678.3, $fixSpaceToNbspFx('€ 12 345 678.30')];
yield [[], ['type' => 'atk4_money'], -12_345_678.3, $fixSpaceToNbspFx('€ -12 345 678.30')];
yield [['decimalSeparator' => ','], ['type' => 'atk4_money'], 12_345_678.3, $fixSpaceToNbspFx('€ 12 345 678,30')];
yield [['thousandsSeparator' => ''], ['type' => 'atk4_money'], 12_345_678.3, $fixSpaceToNbspFx('€ 12345678.30')];
yield [['thousandsSeparator' => ','], ['type' => 'atk4_money'], 12_345_678.3, $fixSpaceToNbspFx('€ 12,345,678.30')];
Expand Down

0 comments on commit 54c8a40

Please sign in to comment.