diff --git a/demos/collection/multitable.php b/demos/collection/multitable.php index 28535320de..b35e226665 100644 --- a/demos/collection/multitable.php +++ b/demos/collection/multitable.php @@ -23,6 +23,19 @@ $finderClass = AnonymousClassNameCache::get_class(fn () => new class() extends Columns { public array $route = []; + /** + * @return list + */ + private function explodeSelectionValue(string $value): array + { + $res = []; + foreach ($value === '' ? [] : explode(',', $value) as $v) { + $res[] = $this->getApp()->uiPersistence->typecastLoadField($this->model->getField($this->model->idField), $v); + } + + return $res; + } + #[\Override] public function setModel(Model $model, array $route = []): void { @@ -34,9 +47,9 @@ public function setModel(Model $model, array $route = []): void $table = Table::addTo($this->addColumn(), ['header' => false, 'class.very basic selectable' => true])->setStyle('cursor', 'pointer'); $table->setModel($model, [$model->titleField]); - $selections = explode(',', $this->getApp()->tryGetRequestQueryParam($this->name) ?? ''); + $selections = $this->explodeSelectionValue($this->getApp()->tryGetRequestQueryParam($this->name) ?? ''); - if ($selections[0]) { + if ($selections !== []) { $table->js(true)->find('tr[data-id=' . $selections[0] . ']')->addClass('active'); } @@ -73,7 +86,7 @@ public function setModel(Model $model, array $route = []): void $table = Table::addTo($this->addColumn(), ['header' => false, 'class.very basic selectable' => true])->setStyle('cursor', 'pointer'); $table->setModel($pushModel->setLimit(10), [$pushModel->titleField]); - if ($selections) { + if ($selections !== []) { $table->js(true)->find('tr[data-id=' . $selections[0] . ']')->addClass('active'); } diff --git a/demos/form-control/input2.php b/demos/form-control/input2.php index 83167a7c4f..19cc4a98b8 100644 --- a/demos/form-control/input2.php +++ b/demos/form-control/input2.php @@ -26,10 +26,10 @@ $group->addControl('line_read', ['readOnly' => true])->set('read only'); $group->addControl('line_disb', ['disabled' => true])->set('disabled'); -$group = $form->addGroup('Text Area'); -$group->addControl('text_norm', [Form\Control\Textarea::class])->set('editable'); -$group->addControl('text_read', [Form\Control\Textarea::class, 'readOnly' => true])->set('read only'); -$group->addControl('text_disb', [Form\Control\Textarea::class, 'disabled' => true])->set('disabled'); +$group = $form->addGroup('Textarea'); +$group->addControl('text_norm', [Form\Control\Textarea::class], ['type' => 'text'])->set("editable\nline2"); +$group->addControl('text_read', [Form\Control\Textarea::class, 'readOnly' => true], ['type' => 'text'])->set("read only\nline2"); +$group->addControl('text_disb', [Form\Control\Textarea::class, 'disabled' => true], ['type' => 'text'])->set("disabled\nline2"); $group = $form->addGroup('Checkbox'); $group->addControl('c_norm', [Form\Control\Checkbox::class], ['type' => 'boolean'])->set(true); @@ -198,25 +198,3 @@ ], ])); $r1->onChange(new JsExpression('console.log(\'radio changed\')')); - -Header::addTo($app, ['Line ends of Textarea']); - -$form = Form::addTo($app); -$group = $form->addGroup('Without model'); -$group->addControl('text_crlf', [Form\Control\Textarea::class])->set("First line\r\nSecond line"); -$group->addControl('text_cr', [Form\Control\Textarea::class])->set("First line\rSecond line"); -$group->addControl('text_lf', [Form\Control\Textarea::class])->set("First line\nSecond line"); - -$group = $form->addGroup('With model'); -$group->addControl('m_text_crlf', [Form\Control\Textarea::class], ['type' => 'text'])->set("First line\r\nSecond line"); -$group->addControl('m_text_cr', [Form\Control\Textarea::class], ['type' => 'text'])->set("First line\rSecond line"); -$group->addControl('m_text_lf', [Form\Control\Textarea::class], ['type' => 'text'])->set("First line\nSecond line"); - -$form->onSubmit(static function (Form $form) { - // check what values are submitted - echo "We're URL encoding submitted values to be able to see what line end is actually submitted."; - foreach ($form->model->get() as $k => $v) { - var_dump([$k => urlencode($v)]); - } - echo 'As you can see - without model it submits CRLF, but with model it will normalize all to LF'; -}); diff --git a/demos/interactive/jssortable.php b/demos/interactive/jssortable.php index d331b3e0af..43b97717e2 100644 --- a/demos/interactive/jssortable.php +++ b/demos/interactive/jssortable.php @@ -37,13 +37,13 @@ $sortable = JsSortable::addTo($view, ['container' => 'ul', 'draggable' => 'li', 'dataLabel' => 'name']); -$sortable->onReorder(static function (array $order, string $src, int $pos, int $oldPos) use ($app) { +$sortable->onReorder(static function (array $orderedNames, string $sourceName, int $pos, int $oldPos) use ($app) { if ($app->tryGetRequestQueryParam('btn')) { - return new JsToast(implode(' - ', $order)); + return new JsToast(implode(' - ', $orderedNames)); } - return new JsToast($src . ' moved from position ' . $oldPos . ' to ' . $pos); -}); + return new JsToast($sourceName . ' moved from position ' . $oldPos . ' to ' . $pos); +}, $model->getField($model->fieldName()->name)); $button = Button::addTo($app)->set('Get countries order'); $button->on('click', $sortable->jsSendSortOrders(['btn' => '1'])); @@ -57,6 +57,6 @@ $grid->setModel((new Country($app->db))->setLimit(6)); $dragHandler = $grid->addDragHandler(); -$dragHandler->onReorder(static function (array $order) { - return new JsToast('New order: ' . implode(' - ', $order)); +$dragHandler->onReorder(static function (array $orderedIds) use ($grid) { + return new JsToast('New order: ' . implode(' - ', array_map(static fn ($id) => $grid->getApp()->uiPersistence->typecastSaveField($grid->model->getField($grid->model->idField), $id), $orderedIds))); }); diff --git a/demos/interactive/loader2.php b/demos/interactive/loader2.php index 4af21fb2d5..397680768b 100644 --- a/demos/interactive/loader2.php +++ b/demos/interactive/loader2.php @@ -30,7 +30,7 @@ $grid->table->onRowClick($countryLoader->jsLoad(['id' => $grid->jsRow()->data('id')])); $countryLoader->set(static function (Loader $p) { - Form::addTo($p)->setModel( - (new Country($p->getApp()->db))->load($p->getApp()->getRequestQueryParam('id')) - ); + $country = new Country($p->getApp()->db); + $id = $p->getApp()->uiPersistence->typecastLoadField($country->getField($country->idField), $p->getApp()->getRequestQueryParam('id')); + Form::addTo($p)->setModel($country->load($id)); }); diff --git a/src/App.php b/src/App.php index c3c0663eed..782cda73a4 100644 --- a/src/App.php +++ b/src/App.php @@ -409,10 +409,8 @@ public function tryGetRequestPostParam(string $key): ?string /** * Get the value of a specific POST parameter from the HTTP request. - * - * @return mixed */ - public function getRequestPostParam(string $key) + public function getRequestPostParam(string $key): string { $res = $this->tryGetRequestPostParam($key); if ($res === null) { diff --git a/src/Form.php b/src/Form.php index 0058ce11b8..6673b38653 100644 --- a/src/Form.php +++ b/src/Form.php @@ -44,10 +44,8 @@ class Form extends View /** * HTML
element, all inner form controls are linked to it on render * with HTML form="form_id" attribute. - * - * @var View */ - public $formElement; + public View $formElement; /** @var Form\Layout A current layout of a form, needed if you call Form->addControl(). */ public $layout; @@ -106,10 +104,10 @@ class Form extends View */ public $controlDisplaySelector = '.field'; - /** @var array Use this apiConfig variable to pass API settings to Fomantic-UI in .api(). */ + /** @var array Use this apiConfig variable to pass API settings to Fomantic-UI in .api(). */ public $apiConfig = []; - /** @var array Use this formConfig variable to pass settings to Fomantic-UI in .from(). */ + /** @var array Use this formConfig variable to pass settings to Fomantic-UI in .from(). */ public $formConfig = []; // {{{ Base Methods @@ -491,7 +489,7 @@ public function fixOwningFormAttrInRenderedHtml(string $html): string * Set Fomantic-UI Api settings to use with form. A complete list is here: * https://fomantic-ui.com/behaviors/api.html#/settings . * - * @param array $config + * @param array $config * * @return $this */ @@ -506,7 +504,7 @@ public function setApiConfig($config) * Set Fomantic-UI Form settings to use with form. A complete list is here: * https://fomantic-ui.com/behaviors/form.html#/settings . * - * @param array $config + * @param array $config * * @return $this */ diff --git a/src/Form/Control.php b/src/Form/Control.php index 81460e8b32..9624205f21 100644 --- a/src/Form/Control.php +++ b/src/Form/Control.php @@ -22,10 +22,10 @@ class Control extends View { /** @var Form|null to which this field belongs */ - public $form; + public ?View $form = null; /** @var EntityFieldPair|null */ - public $entityField; + public ?EntityFieldPair $entityField = null; /** @var string */ public $controlClass = ''; @@ -70,7 +70,7 @@ protected function init(): void { parent::init(); - if ($this->form && $this->entityField) { + if ($this->form !== null && $this->entityField !== null) { if (isset($this->form->controls[$this->entityField->getFieldName()])) { throw (new Exception('Form field already exists')) ->addMoreInfo('name', $this->entityField->getFieldName()); @@ -88,7 +88,7 @@ protected function init(): void #[\Override] public function set($value = null) { - if ($this->entityField) { + if ($this->entityField !== null) { $this->entityField->set($value); } else { $this->content = $value; @@ -101,7 +101,7 @@ public function set($value = null) protected function renderView(): void { // it only makes sense to have "name" property inside a field if used inside a form - if ($this->form) { + if ($this->form !== null) { $this->template->trySet('name', $this->shortName); } diff --git a/src/Form/Control/Checkbox.php b/src/Form/Control/Checkbox.php index d1d4c46089..bd28de8779 100644 --- a/src/Form/Control/Checkbox.php +++ b/src/Form/Control/Checkbox.php @@ -37,7 +37,7 @@ protected function init(): void parent::init(); // checkboxes are annoying because they don't send value when they are not ticked - if ($this->form) { + if ($this->form !== null) { $this->form->onHook(Form::HOOK_LOAD_POST, function (Form $form, array &$postRawData) { if (!isset($postRawData[$this->shortName])) { $postRawData[$this->shortName] = '0'; @@ -53,13 +53,13 @@ protected function renderView(): void $this->template->set('Content', $this->label); } - if ($this->entityField && !is_bool($this->entityField->get() ?? false)) { + if ($this->entityField !== null && !is_bool($this->entityField->get() ?? false)) { throw (new Exception('Checkbox form control requires field with boolean type')) ->addMoreInfo('type', $this->entityField->getField()->type) ->addMoreInfo('value', $this->entityField->get()); } - if ($this->entityField ? $this->entityField->get() : $this->content) { + if ($this->entityField !== null ? $this->entityField->get() : $this->content) { $this->template->dangerouslySetHtml('checked', 'checked="checked"'); } diff --git a/src/Form/Control/Dropdown.php b/src/Form/Control/Dropdown.php index 0611b89316..48b0ed5419 100644 --- a/src/Form/Control/Dropdown.php +++ b/src/Form/Control/Dropdown.php @@ -113,20 +113,17 @@ public function getValue() { // dropdown input tag accepts CSV formatted list of IDs return $this->entityField !== null - ? (is_array($this->entityField->get()) ? implode(', ', $this->entityField->get()) : $this->entityField->get()) + ? (is_array($this->entityField->get()) ? implode(', ', $this->entityField->get()) : $this->entityField->get()) // TODO is_array() should be replaced with field type condition : parent::getValue(); } #[\Override] public function set($value = null) { - if ($this->entityField) { + if ($this->entityField !== null) { if ($this->entityField->getField()->type === 'json' && is_string($value)) { $value = explode(',', $value); } - $this->entityField->set($value); - - return $this; } return parent::set($value); diff --git a/src/Form/Control/Lookup.php b/src/Form/Control/Lookup.php index 8dcebcad8c..82c7bd13dc 100644 --- a/src/Form/Control/Lookup.php +++ b/src/Form/Control/Lookup.php @@ -83,7 +83,7 @@ class Lookup extends Input * * Use this apiConfig variable to pass API settings to Fomantic-UI in .dropdown() * - * @var array + * @var array */ public $apiConfig = ['cache' => false]; @@ -334,7 +334,7 @@ protected function applyDependencyConditions(): void $data = []; if ($this->getApp()->hasRequestQueryParam('form')) { parse_str($this->getApp()->getRequestQueryParam('form'), $data); - } elseif ($this->form) { + } elseif ($this->form !== null) { $data = $this->form->model->get(); } else { return; @@ -386,7 +386,7 @@ protected function renderView(): void $this->initDropdown($chain); - if ($this->entityField && $this->entityField->get()) { + if ($this->entityField !== null && $this->entityField->get() !== null) { $idField = $this->idField ?? $this->model->idField; $this->model = $this->model->loadBy($idField, $this->entityField->get()); diff --git a/src/Form/Control/Radio.php b/src/Form/Control/Radio.php index 5aa6f02020..d072566bde 100644 --- a/src/Form/Control/Radio.php +++ b/src/Form/Control/Radio.php @@ -26,7 +26,7 @@ protected function init(): void parent::init(); // radios are annoying because they don't send value when they are not ticked - if ($this->form) { + if ($this->form !== null) { $this->form->onHook(Form::HOOK_LOAD_POST, function (Form $form, array &$postRawData) { if (!isset($postRawData[$this->shortName])) { $postRawData[$this->shortName] = ''; diff --git a/src/Form/Control/ScopeBuilder.php b/src/Form/Control/ScopeBuilder.php index b4517b86c1..26e799a33c 100644 --- a/src/Form/Control/ScopeBuilder.php +++ b/src/Form/Control/ScopeBuilder.php @@ -272,7 +272,7 @@ protected function init(): void $this->scopeBuilderView = View::addTo($this, ['template' => $this->scopeBuilderTemplate]); - if ($this->form) { + if ($this->form !== null) { $this->form->onHook(Form::HOOK_LOAD_POST, function (Form $form, array &$postRawData) { $key = $this->entityField->getFieldName(); $postRawData[$key] = $this->queryToScope($this->getApp()->decodeJson($postRawData[$key])); @@ -312,7 +312,7 @@ protected function buildQuery(Model $model): void // this is used when selecting proper operator for the inputType (see self::$operatorsMap) $inputsMap = array_column($this->rules, 'inputType', 'id'); - if ($this->entityField && $this->entityField->get() !== null) { + if ($this->entityField !== null && $this->entityField->get() !== null) { $scope = $this->entityField->get(); } else { $scope = $model->scope(); diff --git a/src/Js/JsReload.php b/src/Js/JsReload.php index 0e8737e372..1dd5647275 100644 --- a/src/Js/JsReload.php +++ b/src/Js/JsReload.php @@ -26,6 +26,8 @@ class JsReload implements JsExpressionable /** * Fomantic-UI api settings. * ex: ['loadingDuration' => 1000]. + * + * @var array */ public array $apiConfig = []; @@ -34,6 +36,7 @@ class JsReload implements JsExpressionable /** * @param array $args + * @param array $apiConfig */ public function __construct(View $view, array $args = [], JsExpressionable $afterSuccess = null, array $apiConfig = [], bool $includeStorage = false) { diff --git a/src/JsCallback.php b/src/JsCallback.php index 2daf5fa261..186f8248d8 100644 --- a/src/JsCallback.php +++ b/src/JsCallback.php @@ -17,8 +17,8 @@ class JsCallback extends Callback /** @var string Text to display as a confirmation. Set with setConfirm(..). */ public $confirm; - /** @var array|null Use this apiConfig variable to pass API settings to Fomantic-UI in .api(). */ - public $apiConfig; + /** @var array Use this apiConfig variable to pass API settings to Fomantic-UI in .api(). */ + public $apiConfig = []; /** @var string|null Include web storage data item (key) value to be included in the request. */ public $storeName; @@ -41,7 +41,7 @@ public function jsExecute(): JsBlock 'url' => $this->getJsUrl(), 'urlOptions' => $this->args, 'confirm' => $this->confirm, - 'apiConfig' => $this->apiConfig, + 'apiConfig' => $this->apiConfig !== [] ? $this->apiConfig : null, 'storeName' => $this->storeName, ])]); } diff --git a/src/JsSortable.php b/src/JsSortable.php index dde6b24106..e47435d6e0 100644 --- a/src/JsSortable.php +++ b/src/JsSortable.php @@ -4,6 +4,7 @@ namespace Atk4\Ui; +use Atk4\Data\Field; use Atk4\Ui\Js\JsChain; use Atk4\Ui\Js\JsExpressionable; @@ -65,17 +66,22 @@ protected function init(): void /** * Callback when container has been reorder. * - * @param \Closure(list, string, int, int): (JsExpressionable|View|string|void) $fx + * @param \Closure(list, mixed, int, int): (JsExpressionable|View|string|void) $fx */ - public function onReorder(\Closure $fx): void + public function onReorder(\Closure $fx, Field $idField): void { - $this->set(function () use ($fx) { - $sortOrders = explode(',', $this->getApp()->getRequestPostParam('order')); - $source = $this->getApp()->getRequestPostParam('source'); + $this->set(function () use ($fx, $idField) { + // TODO comma can be in the order/ID value + $orderedIds = explode(',', $this->getApp()->getRequestPostParam('order')); + $sourceId = $this->getApp()->getRequestPostParam('source'); $newIndex = (int) $this->getApp()->getRequestPostParam('newIndex'); $origIndex = (int) $this->getApp()->getRequestPostParam('origIndex'); - return $fx($sortOrders, $source, $newIndex, $origIndex); + $typecastLoadIdFx = fn ($v) => $this->getApp()->uiPersistence->typecastLoadField($idField, $v); + $orderedIds = array_map($typecastLoadIdFx, $orderedIds); + $sourceId = $typecastLoadIdFx($sourceId); + + return $fx($orderedIds, $sourceId, $newIndex, $origIndex); }); } diff --git a/src/Loader.php b/src/Loader.php index 4c8d28c9d7..7b78ac2e93 100644 --- a/src/Loader.php +++ b/src/Loader.php @@ -114,7 +114,8 @@ protected function renderView(): void /** * Return a JS action that will trigger the loader to start. * - * @param string $storeName + * @param array $apiConfig + * @param string $storeName * * @return JsChain */ diff --git a/src/Table/Column/ActionButtons.php b/src/Table/Column/ActionButtons.php index 0343df5036..8dc5adc181 100644 --- a/src/Table/Column/ActionButtons.php +++ b/src/Table/Column/ActionButtons.php @@ -100,7 +100,8 @@ public function addModal($button, $defaults, \Closure $callback, $owner = null, $modal = Modal::addTo($owner, $defaults); $modal->set(function (View $t) use ($callback) { - $callback($t, $t->stickyGet($this->name)); + $id = $this->getApp()->uiPersistence->typecastLoadField($this->table->model->getField($this->table->model->idField), $t->stickyGet($this->name)); + $callback($t, $id); }); return $this->addButton($button, $modal->jsShow(array_merge([$this->name => $this->getOwner()->jsRow()->data('id')], $args)), '', $isDisabled); diff --git a/src/Table/Column/DragHandler.php b/src/Table/Column/DragHandler.php index 3ff67deaae..b3a6dbc0db 100644 --- a/src/Table/Column/DragHandler.php +++ b/src/Table/Column/DragHandler.php @@ -36,11 +36,11 @@ protected function init(): void /** * Callback when table has been reorder using handle. * - * @param \Closure(list, string, int, int): (JsExpressionable|View|string|void) $fx + * @param \Closure(list, mixed, int, int): (JsExpressionable|View|string|void) $fx */ public function onReorder(\Closure $fx): void { - $this->cb->onReorder($fx); + $this->cb->onReorder($fx, $this->table->model->getField($this->table->model->idField)); } #[\Override] diff --git a/src/UserAction/FormExecutor.php b/src/UserAction/FormExecutor.php index 22c85de9ba..84f8be888c 100644 --- a/src/UserAction/FormExecutor.php +++ b/src/UserAction/FormExecutor.php @@ -17,7 +17,7 @@ public function initPreview(): void { $this->addHeader(); - if (!$this->form) { + if ($this->form === null) { $this->form = Form::addTo($this); } diff --git a/src/View.php b/src/View.php index de62624346..24124e8146 100644 --- a/src/View.php +++ b/src/View.php @@ -887,7 +887,7 @@ public function jsAddStoreData(array $data, bool $useSession = false): JsExpress * * @param array $args * @param JsExpressionable|null $afterSuccess - * @param array $apiConfig + * @param array $apiConfig * * @return JsReload */ diff --git a/tests-behat/loader.feature b/tests-behat/loader.feature new file mode 100644 index 0000000000..bed75d0058 --- /dev/null +++ b/tests-behat/loader.feature @@ -0,0 +1,8 @@ +Feature: Loader + + Scenario: + Given I am on "interactive/loader2.php" + When I click using selector "//td[text()='American Samoa']" + Then I check if input value for "input[name='atk_fp_country__iso3']" match text "ASM" + When I click using selector "//td[text()='Argentina']" + Then I check if input value for "input[name='atk_fp_country__iso3']" match text "ARG"