This GridField component allows some pretty advanced functionality; namely, the ability to manually sort items regardless of the type of relation, which is extremely handy. The one major drawback that I've noticed is that if you're attempting to augment/manipulate that relation from any other context then sorting is not handled at all. At least, not until the CMS user happens to navigate to view/load the grid field, at which point there could be unpredictable behavior.
See below for an example.
Steps to Reproduce
For example (this happens to be my use case): Say you have a many-to-many relation from a HomePage which features multiple StoryArticle objects. From the context of the StoryArticle you might want to enable/disable featuring to the HomePage via a checkbox. However, you also need to have explicit and manual control over the sorting of items which get featured there (i.e. via GridField and this component). Also, logically, the idea in this scenario that the act of featuring an article will place this object at the top, thus ->append_to_top is set to true, however this is done from the context of the GridField component.
However, if the user checks the box to feature the story on the homepage, the association will be created, but the sort field (e.g. Sort on the pivot HomePage_FeaturedStoryArticles.Sort) will remain 0 until the user happens to navigate to view the GridField in the CMS, which may be in a completely different area. This is particularly problematic if multiple are configured this way over time and now multiple will sit at 0 and then will be reordered only once the GridField is viewed. This is the "unpredictable" behavior mentioned above, as the sorting result at this point may choose a different order (derived from ID and Created, for example) which may not align with the actual order in which these items were actually associated in time.
Recommendation
Abstract the logic which handles the sorting sufficiently such that it can also be implemented by an extension or at least called statically (e.g. SortableGridField::fixSortRelation($list) or similar). This would allow operations to occur that aren't tightly coupled to a UI layer like GridField such as in the example scenario above.
Believe it or not, this is sort of what I have already been doing for the past 8yrs or so. It's very hacky (and likely extremely inefficient and probably prone to breakage in more complex systems), however, it does at least allow for the ability to invoke functionality built into GridFieldSortableRows in abstract without having to reimplement it nor duplicate configuration directives (like the need to append to the top instead of bottom):
// The only way to get the grid field is to iterate through all fields and checking to see what type of instance it is.
$fields = $parentDataObject->getCMSFields();
foreach($fields->dataFields() as $field) {
if ($field instanceof GridField) {
// Found a grid, let's look for a GridFieldSortableRows component...
foreach($field->getComponents() as $component) {
if ($component instanceof GridFieldSortableRows) {
// Now that we've found our GridField instance AND the sortable row component, we have to set up a form
// which is a hidden dependency behind the scenes which will contain the data object that the author's
//fix needs to reference. So: Set up a fake form so that the grid field can get the DataObject model.
$form = new Form('dontCare', 'doesntMatter', $fields, new FieldList([/* here you go */]));
$form->loadDataFrom($parentDataObject); // The thing it actually needs...
$field->setForm($form);
// Finally now that we have discovered a sortable grid field, let's trigger the *protected*
// ->saveGridRowSort() method (if we have permissions) which will completely rewrite the sort
// numbers based on rules setup in the GridField. To access it, we manually need to
// call ->handleAction() with the CURRENTLY setup results (returned and corrected by
// ->getManipulatedList()). This is useful since ->getManipulatedList() will also call
// ->sortOrderFix() which compensates for the "0" sort field (i.e. put at top or on bottom?).
// once new items are placed on top/bottom, we can then totally reset the sort integers.
try {
$list = $field->getManipulatedList();
$data = ['ItemIDs' => join(',', $list->column("ID"))];
$component->handleAction($field, 'savegridrowsort', [], $data);
} catch(Exception $e) {
// An exception could occur if the currently logged in user doesn't have proper permissions.
logger("Got '" . get_class($e) . "' while attempting to apply sort order fix: " . $e->getMessage(), SS_Log::ERR);
}
// Move on to the next GridField!
continue 2;
}
}
}
}
Of course there's other supporting code which provides the $parentDataObject (the host of the relation) and other checks that prevent operating on it if it's not necessary and etc. However, this at least demonstrates the sort of witchcraft 🧙♂️ 🔮 ✨ I'm using right now in order to workaround this. 😅
Affected Version
All versions of this module and SilverStripe
Description
This GridField component allows some pretty advanced functionality; namely, the ability to manually sort items regardless of the type of relation, which is extremely handy. The one major drawback that I've noticed is that if you're attempting to augment/manipulate that relation from any other context then sorting is not handled at all. At least, not until the CMS user happens to navigate to view/load the grid field, at which point there could be unpredictable behavior.
See below for an example.
Steps to Reproduce
For example (this happens to be my use case): Say you have a many-to-many relation from a
HomePage
which features multipleStoryArticle
objects. From the context of theStoryArticle
you might want to enable/disable featuring to theHomePage
via a checkbox. However, you also need to have explicit and manual control over the sorting of items which get featured there (i.e. viaGridField
and this component). Also, logically, the idea in this scenario that the act of featuring an article will place this object at the top, thus->append_to_top
is set totrue
, however this is done from the context of the GridField component.However, if the user checks the box to feature the story on the homepage, the association will be created, but the sort field (e.g.
Sort
on the pivotHomePage_FeaturedStoryArticles.Sort
) will remain0
until the user happens to navigate to view theGridField
in the CMS, which may be in a completely different area. This is particularly problematic if multiple are configured this way over time and now multiple will sit at0
and then will be reordered only once theGridField
is viewed. This is the "unpredictable" behavior mentioned above, as the sorting result at this point may choose a different order (derived fromID
andCreated
, for example) which may not align with the actual order in which these items were actually associated in time.Recommendation
Abstract the logic which handles the sorting sufficiently such that it can also be implemented by an extension or at least called statically (e.g.
SortableGridField::fixSortRelation($list)
or similar). This would allow operations to occur that aren't tightly coupled to a UI layer likeGridField
such as in the example scenario above.Believe it or not, this is sort of what I have already been doing for the past 8yrs or so. It's very hacky (and likely extremely inefficient and probably prone to breakage in more complex systems), however, it does at least allow for the ability to invoke functionality built into
GridFieldSortableRows
in abstract without having to reimplement it nor duplicate configuration directives (like the need to append to the top instead of bottom):Of course there's other supporting code which provides the
$parentDataObject
(the host of the relation) and other checks that prevent operating on it if it's not necessary and etc. However, this at least demonstrates the sort of witchcraft 🧙♂️ 🔮 ✨ I'm using right now in order to workaround this. 😅