Skip to content

Commit

Permalink
Fix Multiline /w SQL expr (#1818)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek authored Oct 13, 2022
1 parent 4c1c311 commit 1363c65
Show file tree
Hide file tree
Showing 51 changed files with 507 additions and 487 deletions.
25 changes: 19 additions & 6 deletions demos/_demo-data/create-db.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ public function import(array $rowsMulti)
return parent::import(array_map(function (array $rows): array {
$rowsPrefixed = [];
foreach ($rows as $k => $v) {
$rowsPrefixed[$this->prefixFieldName($k)] = $v;
$field = $this->getField($this->prefixFieldName($k));
if (in_array($field->type, ['date', 'time', 'datetime'], true)) {
$v = new \DateTime($v . ' GMT');
}

$rowsPrefixed[$field->shortName] = $v;
}

return $rowsPrefixed;
Expand Down Expand Up @@ -1100,11 +1105,6 @@ public function import(array $rowsMulti)
['id' => 3, 'project_name' => 'Agile Data', 'project_code' => 'at03', 'description' => 'Agile Data implements an entirely new pattern for data abstraction, that is specifically designed for remote databases such as RDS, Cloud SQL, BigQuery and other distributed data storage architectures. It focuses on reducing number of requests your App have to send to the Database by using more sophisticated queries while also offering full Domain Model mapping and Database vendor abstraction.', 'client_name' => 'Agile Toolkit', 'client_address' => 'Some Street,' . "\n" . 'Garden City' . "\n" . 'UK', 'client_country_iso' => 'GB', 'is_commercial' => 0, 'currency' => 'GBP', 'is_completed' => 1, 'project_budget' => 12000, 'project_invoiced' => 0, 'project_paid' => 0, 'project_hour_cost' => 0, 'project_hours_est' => 300, 'project_hours_reported' => 394, 'project_expenses_est' => 600, 'project_expenses' => 430, 'project_mgmt_cost_pct' => 0.2, 'project_qa_cost_pct' => 0.3, 'start_date' => '2016-04-17', 'finish_date' => '2016-06-20', 'finish_time' => '03:04:00', 'created' => '2017-04-06 10:30:15', 'updated' => '2017-04-06 10:35:04'],
['id' => 4, 'project_name' => 'Agile UI', 'project_code' => 'at04', 'description' => 'Web UI Component library.', 'client_name' => 'Agile Toolkit', 'client_address' => 'Some Street,' . "\n" . 'Garden City' . "\n" . 'UK', 'client_country_iso' => 'GB', 'is_commercial' => 0, 'currency' => 'GBP', 'is_completed' => 0, 'project_budget' => 20000, 'project_invoiced' => 0, 'project_paid' => 0, 'project_hour_cost' => 0, 'project_hours_est' => 600, 'project_hours_reported' => 368, 'project_expenses_est' => 1200, 'project_expenses' => 0, 'project_mgmt_cost_pct' => 0.3, 'project_qa_cost_pct' => 0.4, 'start_date' => '2016-09-17', 'finish_date' => '', 'finish_time' => '', 'created' => '2017-04-06 10:30:15', 'updated' => '2017-04-06 10:35:04'],
];
foreach ($data as $rowIndex => $row) {
foreach (['start_date', 'finish_date', 'finish_time', 'created', 'updated'] as $k) {
$data[$rowIndex][$k] = new \DateTime($row[$k] . ' GMT');
}
}
$model->import($data);

$model = new ImportModelWithPrefixedFields($db, ['table' => 'product_category']);
Expand Down Expand Up @@ -1148,4 +1148,17 @@ public function import(array $rowsMulti)
['id' => 7, 'name' => 'Ice Cream', 'brand' => 'Milk Corp.', 'product_category_id' => 3, 'product_sub_category_id' => 8],
]);

$model = new ImportModelWithPrefixedFields($db, ['table' => 'multiline_item']);
$model->addField('item', ['type' => 'string']);
$model->addField('inv_date', ['type' => 'date']);
$model->addField('inv_time', ['type' => 'time']);
$model->addField('country_id', ['type' => 'bigint']);
$model->addField('qty', ['type' => 'integer']);
$model->addField('box', ['type' => 'integer']);
(new Migrator($model))->create();
$model->import([
['id' => 1, 'item' => 'Chocolate', 'inv_date' => '2020-02-20', 'inv_time' => '7:20', 'country_id' => 80, 'qty' => 7, 'box' => 5],
['id' => 2, 'item' => 'DAP delivery', 'inv_date' => '2020-02-01', 'inv_time' => '8:33', 'country_id' => 223, 'qty' => 2, 'box' => 100],
]);

echo 'import complete!' . "\n\n";
92 changes: 18 additions & 74 deletions demos/form-control/multiline.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Atk4\Ui\Demos;

use Atk4\Data\Model;
use Atk4\Data\Persistence;
use Atk4\Ui\Form;
use Atk4\Ui\Header;
use Atk4\Ui\JsExpression;
Expand All @@ -17,73 +15,15 @@

Header::addTo($app, ['Multiline form control', 'icon' => 'database', 'subHeader' => 'Collect/Edit multiple rows of table record.']);

/** @var Model $inventoryItemClass */
$inventoryItemClass = AnonymousClassNameCache::get_class(fn () => new class() extends Model {
public Persistence $countryPersistence;

protected function init(): void
{
parent::init();

$this->addField('item', [
'required' => true,
'default' => 'item',
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->addField('inv_date', [
'default' => new \DateTime(),
'type' => 'date',
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->addField('inv_time', [
'default' => new \DateTime(),
'type' => 'time',
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->hasOne('country', [
'model' => new Country($this->countryPersistence),
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 3]]],
]);
$this->addField('qty', [
'type' => 'integer',
'caption' => 'Qty / Box',
'default' => 1,
'required' => true,
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->addField('box', [
'type' => 'integer',
'caption' => '# of Boxes',
'default' => 1,
'required' => true,
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 2]]],
]);
$this->addExpression('total', [
'expr' => function (Model $row) {
return $row->get('qty') * $row->get('box');
},
'type' => 'integer',
'ui' => ['multiline' => [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']]],
]);
}
});

$inventory = new $inventoryItemClass(new Persistence\Array_(), ['countryPersistence' => $app->db]);

// Populate some data.
$total = 0;
for ($i = 1; $i < 3; ++$i) {
$entity = $inventory->createEntity();
$entity->set('id', $i);
$entity->set('inv_date', new \DateTime());
$entity->set('inv_time', new \DateTime());
$entity->set('item', 'item_' . $i);
$entity->set('country', random_int(1, 100));
$entity->set('qty', random_int(10, 100));
$entity->set('box', random_int(1, 10));
$total += $entity->get('qty') * $entity->get('box');
$entity->saveAndUnload();
}
$inventory = new MultilineItem($app->db);
$inventory->getField($inventory->fieldName()->item)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->inv_date)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->inv_date)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->country_id)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 3]];
$inventory->getField($inventory->fieldName()->qty)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->box)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 2]];
$inventory->getField($inventory->fieldName()->total_sql)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']];
$inventory->getField($inventory->fieldName()->total_php)->ui['multiline'] = [Form\Control\Multiline::TABLE_CELL => ['width' => 1, 'class' => 'blue']];

$form = Form::addTo($app);

Expand All @@ -93,6 +33,10 @@ protected function init(): void
$multiline->setModel($inventory);

// Add total field.
$total = 0;
foreach ($inventory as $item) {
$total += $item->qty * $item->box;
}
$sublayout = $form->layout->addSubLayout([Form\Layout\Section\Columns::class]);
$sublayout->addColumn(12);
$column = $sublayout->addColumn(4);
Expand All @@ -102,19 +46,19 @@ protected function init(): void
$multiline->onLineChange(function (array $rows, Form $form) use ($controlTotal) {
$total = 0;
foreach ($rows as $row => $cols) {
$qty = $cols['qty'] ?? 0;
$box = $cols['box'] ?? 0;
$total += $qty * $box;
$total += $cols[MultilineItem::hinting()->fieldName()->qty] * $cols[MultilineItem::hinting()->fieldName()->box];
}

return $controlTotal->jsInput()->val($total);
}, ['qty', 'box']);
}, [$inventory->fieldName()->qty, $inventory->fieldName()->box]);

$multiline->jsAfterAdd = new JsFunction(['value'], [new JsExpression('console.log(value)')]);
$multiline->jsAfterDelete = new JsFunction(['value'], [new JsExpression('console.log(value)')]);

$form->onSubmit(function (Form $form) use ($multiline) {
$rows = $multiline->saveRows()->model->export();
$rows = $multiline->model->atomic(function () use ($multiline) {
return $multiline->saveRows()->model->export();
});

return new JsToast($form->getApp()->encodeJson(array_values($rows)));
});
41 changes: 41 additions & 0 deletions demos/init-db.php
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,44 @@ protected function init(): void
])->addTitle();
}
}

/**
* @property string $item @Atk4\Field()
* @property \DateTime $inv_date @Atk4\Field()
* @property \DateTime $inv_time @Atk4\Field()
* @property Country $country_id @Atk4\RefOne()
* @property int $qty @Atk4\Field()
* @property int $box @Atk4\Field()
* @property int $total_sql @Atk4\Field()
* @property int $total_php @Atk4\Field()
*/
class MultilineItem extends ModelWithPrefixedFields
{
public $table = 'multiline_item';

protected function init(): void
{
parent::init();

$this->addField($this->fieldName()->item, ['required' => true]);
$this->addField($this->fieldName()->inv_date, ['type' => 'date']);
$this->addField($this->fieldName()->inv_time, ['type' => 'time']);
$this->hasOne($this->fieldName()->country_id, [
'model' => [Country::class],
]);
$this->addField($this->fieldName()->qty, ['type' => 'integer', 'required' => true]);
$this->addField($this->fieldName()->box, ['type' => 'integer', 'required' => true]);
$this->addExpression($this->fieldName()->total_sql, [
'expr' => function (Model /* TODO self is not working bacause of clone in Multiline */ $row) {
return $row->expr('{' . $this->fieldName()->qty . '} * {' . $this->fieldName()->box . '}'); // @phpstan-ignore-line
},
'type' => 'integer',
]);
$this->addCalculatedField($this->fieldName()->total_php, [
'expr' => function (self $row) {
return $row->qty * $row->box;
},
'type' => 'integer',
]);
}
}
10 changes: 5 additions & 5 deletions js/src/plugins/conditional-form.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ export default class AtkConditionalFormPlugin extends AtkPlugin {
}
// add change listener to inputs according to selector
this.$el.find(':checkbox')
.on('change', this, atk.debounce(this.onInputChange, 100, true));
.on('change', this, atk.createDebouncedFx(this.onInputChange, 100, true));
this.$el.find(':radio')
.on('change', this, atk.debounce(this.onInputChange, 100, true));
.on('change', this, atk.createDebouncedFx(this.onInputChange, 100, true));
this.$el.find('input[type="hidden"]')
.on('change', this, atk.debounce(this.onInputChange, 100, true));
.on('change', this, atk.createDebouncedFx(this.onInputChange, 100, true));
this.$el.find('input')
.on(this.settings.validateEvent, this, atk.debounce(this.onInputChange, 250));
.on(this.settings.validateEvent, this, atk.createDebouncedFx(this.onInputChange, 250));
this.$el.find('select')
.on('change', this, atk.debounce(this.onInputChange, 100));
.on('change', this, atk.createDebouncedFx(this.onInputChange, 100));

this.initialize();
}
Expand Down
4 changes: 2 additions & 2 deletions js/src/plugins/js-search.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class AtkJsSearchPlugin extends AtkPlugin {
* Query server on each keystroke after proper timeout.
*/
onAutoQueryAction() {
this.textInput.on('keyup', atk.debounce((e) => {
this.textInput.on('keyup', atk.createDebouncedFx((e) => {
const options = $.extend({}, this.urlArgs, this.settings.urlOptions);
if (e.target.value === '' || e.keyCode === 27) {
this.doSearch(this.settings.url, null, options, () => {
Expand Down Expand Up @@ -205,6 +205,6 @@ AtkJsSearchPlugin.DEFAULTS = {
urlQueryKey: null,
q: null,
autoQuery: false,
timeOut: 300,
timeOut: 250,
useAjax: true,
};
6 changes: 6 additions & 0 deletions js/src/plugins/js-sortable.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export default class AtkJsSortablePlugin extends AtkPlugin {
// the original index value of the dragged element.
this.orgIdx = null;

// fix screen reader announcement container added more than once
// https://github.com/Shopify/draggable/pull/541
for (let elem; elem = document.getElementById('draggable-live-region');) { // eslint-disable-line no-cond-assign
elem.remove();
}

this.injectStyles(this.settings.mirrorCss + this.settings.overCss);
this.dragContainer = this.$el.find(this.settings.container);
const sortable = new Draggable.Sortable(
Expand Down
15 changes: 3 additions & 12 deletions js/src/plugins/scroll.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import AtkPlugin from './atk.plugin';
* padding: 20 The amount of padding needed prior to request a page load.
* initialPage: 1 The initial page load when calling this plugin.
* appendTo: null The html element where new content should be append to.
* allowJsEval: false Whether or not javascript send in server response should be evaluate.
* stateContext: null A jQuery selector, where you would like Fomantic-UI, to apply the stateContext to during the api call. if null, then a default loader will be apply to the bottom of the $inner element.
*/
export default class AtkScrollPlugin extends AtkPlugin {
Expand All @@ -23,7 +22,6 @@ export default class AtkScrollPlugin extends AtkPlugin {
padding: 20,
initialPage: 1,
appendTo: null,
allowJsEval: false,
hasFixTableHeader: false,
tableContainerHeight: 400,
tableHeaderColor: '#ffffff',
Expand Down Expand Up @@ -165,14 +163,10 @@ export default class AtkScrollPlugin extends AtkPlugin {
this.removeLoader();
if (response.success) {
if (response.html) {
// Done - no more pages
if (response.message === 'Done') {
this.$target.append(response.html);
this.$target.append(response.html);
if (response.noMoreScrollPages) {
this.idle();
}
// Success - will have more pages
if (response.message === 'Success') {
this.$target.append(response.html);
} else {
this.isWaiting = false;
this.nextPage++;
// if there is no scrollbar, then try to load next page too
Expand All @@ -183,9 +177,6 @@ export default class AtkScrollPlugin extends AtkPlugin {
}

response.id = null;
if (!this.settings.options.allowJsEval) {
response.atkjs = null;
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions js/src/services/panel.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ class PanelService {
*/
addClickAwayEvent(id) {
// clicking anywhere in main tag will close panel.
$('main').on('click.atkPanel', atk.debounce((evt) => {
$('main').on('click.atkPanel', atk.createDebouncedFx((evt) => {
this.closePanel(id);
}, 250));
}
Expand All @@ -290,7 +290,7 @@ class PanelService {
*/
addEscAwayEvent(id) {
// pressing esc key will close panel.
$(document).on('keyup.atkPanel', atk.debounce((evt) => {
$(document).on('keyup.atkPanel', atk.createDebouncedFx((evt) => {
if (evt.keyCode === 27) {
this.closePanel(id);
}
Expand Down
10 changes: 5 additions & 5 deletions js/src/services/vue.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ const componentFactory = (name, component) => () => ({
});

const atkComponents = {
'atk-inline-edit': componentFactory('atk-inline-edit', () => import(/* webpackChunkName: "atk-vue-inline-edit" */'../components/inline-edit.component')),
'atk-item-search': componentFactory('atk-item-search', () => import(/* webpackChunkName: "atk-vue-item-search" */'../components/item-search.component')),
'atk-multiline': componentFactory('atk-multiline', () => import(/* webpackChunkName: "atk-vue-multiline" */'../components/multiline/multiline.component')),
'atk-tree-item-selector': componentFactory('atk-tree-item-selector', () => import(/* webpackChunkName: "atk-vue-tree-item-selector" */'../components/tree-item-selector/tree-item-selector.component')),
'atk-query-builder': componentFactory('atk-query-builder', () => import(/* webpackChunkName: "atk-vue-query-builder" */'../components/query-builder/query-builder.component.vue')),
'atk-inline-edit': componentFactory('atk-inline-edit', () => import(/* webpackChunkName: "atk-vue-inline-edit" */'../vue-components/inline-edit.component')),
'atk-item-search': componentFactory('atk-item-search', () => import(/* webpackChunkName: "atk-vue-item-search" */'../vue-components/item-search.component')),
'atk-multiline': componentFactory('atk-multiline', () => import(/* webpackChunkName: "atk-vue-multiline" */'../vue-components/multiline/multiline.component')),
'atk-tree-item-selector': componentFactory('atk-tree-item-selector', () => import(/* webpackChunkName: "atk-vue-tree-item-selector" */'../vue-components/tree-item-selector/tree-item-selector.component')),
'atk-query-builder': componentFactory('atk-query-builder', () => import(/* webpackChunkName: "atk-vue-query-builder" */'../vue-components/query-builder/query-builder.component.vue')),
};

/**
Expand Down
20 changes: 10 additions & 10 deletions js/src/setup-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ atk.eventBus = (function () {
};
}());

atk.debounce = function (func, wait, options) {
atk.createDebouncedFx = function (func, wait, options) {
let timerId = null;
let debouncedInner;
let lodashDebouncedFx;

function createTimer() {
timerId = setInterval(() => {
if (!debouncedInner.pending()) {
if (!lodashDebouncedFx.pending()) {
clearInterval(timerId);
timerId = null;
$.active--;
Expand All @@ -54,20 +54,20 @@ atk.debounce = function (func, wait, options) {
$.active++;
}

debouncedInner = lodashDebounce(func, wait, options);
lodashDebouncedFx = lodashDebounce(func, wait, options);

function debounced(...args) {
function debouncedFx(...args) {
if (timerId === null) {
createTimer();
}

return debouncedInner(...args);
return lodashDebouncedFx(...args);
}
debounced.cancel = debouncedInner.cancel;
debounced.flush = debouncedInner.flush;
debounced.pending = debouncedInner.pending;
debouncedFx.cancel = lodashDebouncedFx.cancel;
debouncedFx.flush = lodashDebouncedFx.flush;
debouncedFx.pending = lodashDebouncedFx.pending;

return debounced;
return debouncedFx;
};

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import $ from 'external/jquery';

/**
* Vue component
* Allow user to edit a db record inline and send
* changes to server.
*
Expand Down
Loading

0 comments on commit 1363c65

Please sign in to comment.