Recently I’ve been working a lot with the SilverStripe CMS/framework written in PHP (as one could undoubtedly deduce from my earlier post). The context involved porting a custom application from SilverStripe 2.4 to 3.1. Naturally this also involved dealing with the formerly used DataObjectManager module and 3.x’s new ORM and GridField components.
DataObjectManager, being an external module, compensated for a number of shortcomings in SilverStripe 2.4’s built-in components. The exact set of features it had is quite hard to assess from today’s perspective because Web resources discussing it have become scarce. At the same time, however, it seems to have eventually become so bloated that it didn’t get ported to SilverStripe 3.0. Instead the new GridField components present a radically opposite approach: simple, clean, modular.
Now here’s the thing. With DataObjectManager, it seems one didn’t really have to care about an object’s relation type or the underlying SQL query, although one could augment it eg. with additional JOIN clauses. A statement such as the following just worked:
$dom = new DataObjectManager( $this, 'Spoons', 'Spoon', array( 'SpoonCategory' => 'Category', 'Image_CMSThumbnail' => 'Photo', 'Title' => 'Title' ), 'getCMSFields_forPopup', null, 'SpoonCategory.SortOrder', 'LEFT JOIN [...]' );
Now with GridField, things are different. To get at least a subset of the DataObjectManager’s functionality, I have to decide on one of two base GridField configurations: either
However this is not entirely correct respectively not a complete answer.
To examine this closer, let’s first have a look at an example and some screenshots. Suppose we defined a page type Project with a has_many relation to a Student DataObject (see SilverStripe’s DataObject Relationship Management tutorial) and we created a GridField for this relation.
Here’s the GridField in a RecordEditor configuration (this is with a German locale, you’ll recognize the big green button as the “Add” button):
Here’s the same GridField in a RelationEditor configuration:
Remember the differences you see for a moment as we have a look at the actual code and diff the two. RecordEditor is on the left, RelationEditor on the right:
The code differences are:
- RelationEditor adds a GridFieldAddExistingAutocompleter component.
- RelationEditor creates the GridFieldDeleteAction component with the parameter true.
GridFieldAddExistingAutocompleter is a combination of a Textfield, that allows to search for existing, matching DataObjects, and a button to add a selected match to the GridField (and thus its underlying relation). You saw it in the second screenshot to the upper right of the GridField. We’ll learn more about its use in just a second.
GridFieldDeleteAction‘s constructor looks like this:
public function __construct($removeRelation = false) { [...] }
So RelationEditor sets the internal variable $removeRelation to true, affecting the following method:
public function getColumnContent($gridField, $record, $columnName) { if($this->removeRelation) { [...] $field = GridField_FormAction::create([...], "unlinkrelation", [...]) [...] } else { [...] $field = GridField_FormAction::create([...] "deleterecord", [...]) [...] } return $field->Field(); }
Thus, in the RecordEditor config each row will have a deleterecord action whereas with the RelationEditor config it will be unlinkrelation. These action titles pretty much describe what they do: the former really deletes the records, ie. removes the DataObject in question from the database, the latter only removes it from a relation.
This has some not necessarily intuitive consequences that can be quite surprising to the CMS users.
If we were to stick to what the documentation says, we’d use a RelationEditor GridField config because, as said above, the GridField shows data from a has_many relation. So remember the second screenshot from above. Suppose there is no separate “Users” page, instead users get created along as soon as they’re added to a project. Let’s say we just created and added the student “Joe” this way.
Now we want to remove him again. The icon to the right shows a chain symbol with a red dash, indicative of the unlinkrelation action, but your ordinary day CMS user probably won’t even notice. Clicking it will remove “Joe” from this project and, unless “Joe” was part of any other project (in which case has_many would be wrong, many_many would be correct), also make “Joe” invisible at any other page. The user would rightfully assume that “Joe” is no longer part of the database.
Now suppose some time later he wants to add “Joe” again, to the same project. He could unwittingly use the “Add” button, enter the name “Joe”, voila, would seem to work. But he could have a suspicion and use the AddExistingAutoCompleter field and type “J”:
Doh! Suddenly there are two “Joes” as a short SELECT * FROM Student WHERE Name="Joe" would confirm. The reason being that the first (old) “Joe” never got deleted, only removed from the relation in question.
Please note that this is strictly speaking not a “bug” in the RelationEditor config. If you take relation editing literally, unlinking (and not deleting) is exactly what one would expect. You could say, to really get rid of the user is a job of the RecordEditor config.
However this doesn’t really stand a serious discussion:
- Insisting on the use of RelationEditor means that you will always have to add a second, separate GridField in RelationEditor configuration for the related DataObject, either on a separate tab or even promoted to be a subclass of Page so it can have its own entry in the SiteTree. For the example above, we’d nee a separate “Students” GridField, which would do as we originally expected, really delete students.
- You could argue that this is the way to go because it would be more consistent: a project gives an embodying context, removing students here intuitively removes them from the project only. Whereas removing a student from the “Students” GridField removes them completely. But then the same would have to hold true for adding: you would rightfully expect that clicking a project’s “Add” button adds already existing students only. Wrong! A RelationEditor‘s “Add” button on the contrary adds completely new DataObjects only! To add existing ones, you need to use the AddExistingAutoCompleter. Consistency look different.
- Yes, the different configurations indicate the different types of “removal” through different icons. But you don’t really expect your average CMS user to really intuitively understand the difference, even if there would be a dedicated RecordEditor GridField, would you?
So obviously the out-of-the-box GridField components won’t really provide me with a satisfactory solution. The question then remains: what would be satisfactory?
Actually I’m not sure yet.
This page suggests to just add both action buttons, unlinkrelation and deleterecord, but that would just defer the problem to the user, causing confused users to keep creating “zombie records”.
One idea could be to provide an “intelligent” delete action that would only unlink (and show the chain icon) as long as the “deleted” DataObject were still visible in some other part of the CMS and delete (and show the delete icon) if not. But how would you know? There is no reference count or anything.
Another idea is to use a RecordEditor configuration along with a separate column “Linked” and a special button to toggle between a “show only linked items” filter and “show all items”. That’s what DataObjectManager provided, but apart from the fact that DataObjectManager lives in the SilverStripe 2.x world only, it’s author himself raised concerns about that concept’s usability, seeing that checkboxes would be the tool of choice for selecting items for batch actions. Having another row of checkboxes could thus confuse users just as well.
So that’s where I stand right now. Sort of at a usability puzzle.
I completely agree with the usability issues you’ve addressed here. I usually create ModelAdmin’s for individual DataObjects, and then use the RelationEditor to create the relationships. This *still* requires 2 screens to manage DOs, and is not intuitive.
Yes. In my case I can kinda justified having both editor types because I have more than one page where I relate the DataObject in question. And the DataObjects are usually only added and never deleted, just unlinked, because there’s a high probability that they get linked again in the future, ie. the DataObjects themselves are quite static, just the relation isn’t.
However the GUI madness goes on… if a DataObject’s canDelete() returns false for a single record, the delete button does not show up at all, causing the editor button to slide right, visually aligned unter the delete buttons of the other records. A visual mess that also leaves the user clueless. I fixed this by extending GridFieldDeleteAction with a custom getColumnContent() method which instead displays a black red cross and shows an informative popup when clicked, why the delete is not possible.