Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW Nested gridfield #384

Merged
merged 26 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
db3edd7
NEW Initial nested gridfield component
forsdahl Apr 12, 2024
27dc636
Styling for nested GridFields without title.
forsdahl Apr 15, 2024
19a7cff
Added initial unit test for GridFieldNestedForm
forsdahl Apr 15, 2024
bcc4ea1
Added initial user documentation for Nested GridFields.
forsdahl Apr 15, 2024
654b729
Renamed GridFieldNestedForm_ItemRequest, to conform with coding styles.
forsdahl Apr 18, 2024
c043220
Changed base-class of GridFieldNestedForm, it doesn't share much with
forsdahl Apr 18, 2024
c415d43
Fixed linting issues
forsdahl Apr 18, 2024
f8c777d
Changed naming schema for nested GridFields, to not include [ or ]
forsdahl Apr 18, 2024
4fc20fb
Added one more unit test for GridFieldNestedForm
forsdahl Apr 18, 2024
fc40420
Added configurable max nesting level for nested GridFields
forsdahl Apr 18, 2024
32d980e
Fixed moving nested gridfield items to other gridfields
forsdahl Apr 23, 2024
cfcf8d2
Refactored nested GridField move to parent functionality.
forsdahl Apr 23, 2024
847ce07
Fixes and some refactoring for max nesting level handling in
forsdahl Apr 24, 2024
f7b8aea
PHPDoc additions and linting fixes for gridfield nested form
forsdahl Apr 24, 2024
c517c69
Don't assume records are DataObjects in nested gridfields.
forsdahl Apr 24, 2024
70b838e
Removed legacy disabling of security token and strict form method check
forsdahl Apr 24, 2024
5e60972
Added phpdoc for nested grid field item request handler class.
forsdahl Apr 24, 2024
bc1180b
Throw exception in nested gridfields if the relation is invalid.
forsdahl Apr 24, 2024
46e5ccc
Changed some PHPDoc return types to real typehings in nested gridfield.
forsdahl Apr 24, 2024
1ad6acb
Refactored grid field nested form link to be a button with aria-attri…
forsdahl Apr 24, 2024
1993acb
Linting and typehinting fixes for nested grid field
forsdahl May 7, 2024
9ab3ed6
Throw an exception in Nesed Gridfield if an invalid relation is confi…
forsdahl May 7, 2024
8f50565
Only add nested form to nested gridfield child if that child is of
forsdahl May 7, 2024
a9b0a70
Throw 404 error on grid field nested form move-to-parent action,
forsdahl May 7, 2024
c476ced
Added more documentation for nested gridfields.
forsdahl May 15, 2024
a42cd43
Only allow one GridFieldNestedForm component per GridField.
forsdahl May 15, 2024
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This module provides a number of useful grid field components:
features.
* `GridFieldTitleHeader` - a simple header which displays column titles.
* `GridFieldConfigurablePaginator` - a paginator for GridField that allows customisable page sizes.
* `GridFieldNestedForm` - allows nesting of GridFields for managing relation records directly within
a parent GridField.

## Installation

Expand Down
24 changes: 24 additions & 0 deletions css/GridFieldExtensions.css
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,27 @@
.grid-field-inline-new--multi-class-list__visible {
display: block;
}

/**
* GridFieldNestedForm
*/
.grid-field tr.nested-gridfield td.gridfield-holder {
padding-left: 60px;
}

.grid-field.nested.empty-title .grid-field__title-row th {
padding: 0;
}

.grid-field.nested table tbody tr:not(.nested-gridfield) {
border-left: 1px solid #dbe0e9;
}

.grid-field.nested table tbody tr:not(.nested-gridfield).last {
border-bottom: 1px solid #dbe0e9;
}

.ss-gridfield-orderable.has-nested > .grid-field__table > .ss-gridfield-items > .ss-gridfield-item.ui-droppable-active.ui-state-highlight {
border: 0;
background-color: #fbf9ee;
}
43 changes: 43 additions & 0 deletions docs/en/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,46 @@ $paginator->setItemsPerPage(500);

The first shown record will be maintained across page size changes, and the number of pages and current page will be
recalculated on each request, based on the current first shown record and page size.

Nested GridFields
-----------------

The `GridFieldNestedForm` component allows you to nest GridFields in the UI. It can be used with `DataObject` subclasses
with the `Hierarchy` extension, or by specifying the relation used for nesting.

```php
// Basic usage, defaults to the Children-method for Hierarchy objects.
$grid->getConfig()->addComponent(GridFieldNestedForm::create());

// Usage with custom relation
$grid->getConfig()->addComponent(GridFieldNestedForm::create()->setRelationName('MyRelation'));
```

You can define your own custom GridField config for the nested GridField configuration by implementing a `getNestedConfig`
on your nested model (should return a `GridField_Config` object).
```php
class NestedObject extends DataObject
{
private static $has_one = [
'Parent' => ParentObject::class
];

public function getNestedConfig(): GridFieldConfig
{
$config = new GridFieldConfig_RecordViewer();
return $config;
}
}
```

You can also modify the default config (a `GridFieldConfig_RecordEditor`) via an extension to the nested model class, by implementing
`updateNestedConfig`, which will get the config object as the first parameter.
```php
class NestedObjectExtension extends DataExtension
{
public function updateNestedConfig(GridFieldConfig &$config)
{
$config->removeComponentsByType(GridFieldPaginator::class);
}
}
```
171 changes: 171 additions & 0 deletions javascript/GridFieldExtensions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
(function($) {
let preventReorderUpdate = false;
let updateTimeouts = [];

$.entwine("ss", function($) {
/**
* GridFieldAddExistingSearchButton
Expand Down Expand Up @@ -510,5 +513,173 @@
this.parent().find('.ss-gridfield-pagesize-submit').trigger('click');
}
});

/**
* GridFieldNestedForm
*/
$('.grid-field .col-listChildrenLink button').entwine({
onclick: function(e) {
let gridField = $(this).closest('.grid-field');
let currState = gridField.getState();
let toggleState = false;
let pjaxTarget = $(this).attr('data-pjax-target');
if ($(this).hasClass('font-icon-right-dir')) {
toggleState = true;
}
if (typeof currState['GridFieldNestedForm'] == 'undefined' || currState['GridFieldNestedForm'] == null) {
currState['GridFieldNestedForm'] = {};
}
currState['GridFieldNestedForm'][$(this).attr('data-pjax-target')] = toggleState;
gridField.setState('GridFieldNestedForm', currState['GridFieldNestedForm']);
if (toggleState) {
if (!$(this).closest('tr').next('.nested-gridfield').length) {
// add loading indicator until the nested gridfield is loaded
let colspan = gridField.find('.grid-field__title-row th').attr('colspan');
let loadingCell = $('<td />')
.addClass('ss-gridfield-item loading')
.attr('colspan', colspan);
$(this).closest('tr').after($('<tr class="nested-gridfield" />').append(loadingCell));

let data = {};
let stateInput = gridField.find('input.gridstate').first();
data[stateInput.attr('name')] = JSON.stringify(currState);
if (window.location.search) {
let searchParams = window.location.search.replace('?', '').split('&');
for (let i = 0; i < searchParams.length; i++) {
let parts = searchParams[i].split('=');
data[parts[0]] = parts[1];
}
}
$.ajax({
type: 'POST',
url: $(this).attr('data-url'),
data: data,
headers: {
'X-Pjax': pjaxTarget
},
success: function(data) {
if (data && data[pjaxTarget]) {
gridField.find(`[data-pjax-fragment="${pjaxTarget}"]`).replaceWith(data[pjaxTarget]);
}
}
});
}
else {
$(this).closest('tr').next('.nested-gridfield').show();
$.ajax({
url: $(this).attr('data-toggle')+'1'
});
}
$(this).removeClass('font-icon-right-dir');
$(this).addClass('font-icon-down-dir');
$(this).attr('aria-expanded', 'true');
}
else {
$.ajax({
url: $(this).attr('data-toggle')+'0'
});
$(this).closest('tr').next('.nested-gridfield').hide();
$(this).removeClass('font-icon-down-dir');
$(this).addClass('font-icon-right-dir');
$(this).attr('aria-expanded', 'false');
}
e.preventDefault();
e.stopPropagation();
return false;
}
});

// move nested gridfields onto their own rows below this row, to make it look nicer
$('.col-listChildrenLink > .grid-field.nested').entwine({
onadd: function() {
let nrOfColumns = $(this).closest('tr').children('td').length;
let evenOrOdd = 'even';
if ($(this).closest('tr').hasClass('odd')) {
evenOrOdd = 'odd';
}
if ($(this).closest('.grid-field').hasClass('editable-gridfield')) {
$(this).find('tr').removeClass('even').removeClass('odd').addClass(evenOrOdd);
}

if ($(this).closest('tr').next('tr.nested-gridfield').length) {
$(this).closest('tr').next('tr.nested-gridfield').remove();
}

// add a new table row, with one table cell which spans all columns
$(this).closest('tr').after('<tr class="nested-gridfield '+evenOrOdd+'"><td class="gridfield-holder" colspan="'+nrOfColumns+'"></td></tr>');
// move this field into the newly created row
$(this).appendTo($(this).closest('tr').next('tr').find('td').first());
$(this).show();
this._super();
}
});

$('.ss-gridfield-orderable.has-nested > .grid-field__table > tbody, .ss-gridfield-orderable.nested > .grid-field__table > tbody').entwine({
onadd: function() {
this._super();
let gridField = this.getGridField();
if (gridField.data("url-movetoparent")) {
let parentID = 0;
let parentItem = gridField.closest('.nested-gridfield').prev('.ss-gridfield-item');
if (parentItem && parentItem.length) {
parentID = parentItem.attr('data-id');
}
this.sortable('option', 'connectWith', '.ss-gridfield-orderable tbody');
this.sortable('option', 'start', function(e, ui) {
if (ui.item.find('.col-listChildrenLink').length && ui.item.next('.ui-sortable-placeholder').next('.nested-gridfield').length) {
if (ui.item.find('.col-listChildrenLink a').hasClass('font-icon-down-dir')) {
ui.item.find('.col-listChildrenLink a').removeClass('font-icon-down-dir');
ui.item.find('.col-listChildrenLink a').addClass('font-icon-right-dir');
}
ui.item.next('.ui-sortable-placeholder').next('.nested-gridfield').remove();
let pjaxFragment = ui.item.find('.col-listChildrenLink a').attr('data-pjax-target');
ui.item.find('.col-listChildrenLink').append(`<div class="nested-container" data-pjax-fragment="${pjaxFragment}" style="display:none;"></div>`);
}
});
this.sortable('option', 'receive', function(e, ui) {
preventReorderUpdate = true;
while (updateTimeouts.length) {
let timeout = updateTimeouts.shift();
window.clearTimeout(timeout);
}
let childID = ui.item.attr('data-id');
let parentIntoChild = $(e.target).closest('.grid-field[data-name*="-GridFieldNestedForm-'+childID+'"]').length;
if (parentIntoChild) {
// parent dragged into child, cancel sorting
ui.sender.sortable("cancel");
e.preventDefault();
e.stopPropagation();
window.setTimeout(function() {
preventReorderUpdate = false;
}, 500);
return false;
}
let sortInput = ui.item.find('input.ss-orderable-hidden-sort');
let sortName = sortInput.attr('name');
let index = sortName.indexOf('[GridFieldEditableColumns]');
sortInput.attr('name', gridField.attr('data-name')+sortName.substring(index));
gridField.find('> .grid-field__table > tbody').rebuildSort();
gridField.reload({
url: gridField.data("url-movetoparent"),
data: [
{ name: "move[id]", value: childID},
{ name: "move[parent]", value: parentID}
]
}, function() {
preventReorderUpdate = false;
});
});
let updateCallback = this.sortable('option', 'update');
this.sortable('option', 'update', function(e, ui) {
if (!preventReorderUpdate) {
let timeout = window.setTimeout(function() {
updateCallback(e, ui);
}, 500);
updateTimeouts.push(timeout);
}
});
}
}
});
});
})(jQuery);
Loading
Loading