Closed maxime-rainville closed 1 year ago
Link to my first look at the module which includes a list of maintenance tasks to perform
Here I've had done a deeper look at the code. I've assumed that project implementations are done via adding the $has_one
version of linkfield to a Page
dataobject.
Link::canView()
+ other can*() methods are missing - though this doesn't really seem like an issue .. since they're saved as as part of page save process they essentially get just get the same permissions as the page they're oncanView()/canEdit()/etc
checks there.canView()
permission checks inside the Link subclass generateLinkDescription()
methods where they use DataLists
.RequiredFields
validation doesn't work #69PhoneLink
and it now there's frontend validation:
public function getCMSCompositeValidator(): CompositeValidator
{
$validator = parent::getCMSCompositeValidator();
$validator->addValidator(RequiredFields::create([
'Phone',
]));
return $validator;
}
private static array $extensions = [Versioned::class];
to Link
private static array $has_one = ['HasOneLink' => Link::class];
on Page
, add private static array $owns = ['HasOneLink'];
and also the same for $cascade_deletes
and $cadcade duplicates
.Internal slack conversation asking developers about what they perceive as feature gaps - https://silverstripeltd.slack.com/archives/CLXKD9X51/p1695690436734529. tl;dr:
ORM/DBJson.php
/ ORM/DBLink.php
and friends will be deleted - see notes below in "Notes about the 2x implementations"AjaxField/FormFactoryExtension/LeftAndMain/ModalController
) in the 'Extensions' directory that are a bit messy which support a 'DynamicLink' used to dynamically provide a JSON form schema. On AjaxField
there's a comment "There's probably a less dumb way of doing this". This could use a tidy up. LeftAndMain needs to be renamed to LeftAndMainExtension :-)Registry
class and _config/types.yml
is used to define the link types, though it's unnecessary we can just look for subclasses of Link::class
. _config/types.yml
does allow you to se enabled: false
to globally disable link types, though a better way would be to just have a private static $enabled
on Link and just config it off on the subclasses you don't want. Also I'd much rather just have LinkField.php::setAllowTypes()/setDenyTypes()
methods directly on the LinkField
to specific allowed link types in a given contextJsonData
and Type
interface are both only used on the Link class, which is a base class that is subclassed. Just get rid of the interfaces since they're pointless abstraction.LinkField.js::stringifyData()
before submitting as a single field via POST on the page edit form, where it's then decoded on the server and turned into a related dataobject. It's kind of like doing a REST API call done via POST instead of XHR.INIT_FORM_SCHEMA_STACK
action which presumably does something in asset-adminentwine/JsonField.js
<Fragment>
in theresetData()
though otherwise it seems generally OK, could be improvedLinkPicker.js
Faster database performance, though uses controversial JSON data in db field - makes finding data via DB queries much harder. We don't do this anywhere else in Silverstripe.
private static array $db = [
'DbLink' => DBLink::class
];
mysql> select * from Page where ID = 2;
+----+----------+-----------------------------------------------------------------------------------------------------+--------------+
| ID | MyFileID | DbLink | HasOneLinkID |
+----+----------+-----------------------------------------------------------------------------------------------------+--------------+
| 2 | 0 | {"Root":null,"Main":null,"Title":"My db link","PageID":3,"Anchor":"","OpenInNew":0,"typeKey":"cms"} | 1 |
+----+----------+-----------------------------------------------------------------------------------------------------+--------------+
Slower database performance as it needs to join a bunch of tables together, though consistent with the rest of Silverstripe
private static array $has_one = [
'HasOneLink' => Link::class,
];
mysql> select * from LinkField_Link;
+----+--------------------------------------------+---------------------+---------------------+-----------------+-----------+
| ID | ClassName | LastEdited | Created | Title | OpenInNew |
+----+--------------------------------------------+---------------------+---------------------+-----------------+-----------+
| 1 | SilverStripe\LinkField\Models\SiteTreeLink | 2023-09-14 15:55:03 | 2023-09-14 15:55:03 | My Has one link | 0 |
+----+--------------------------------------------+---------------------+---------------------+-----------------+-----------+
mysql> select * from LinkField_SiteTreeLink;
+----+--------+--------+
| ID | Anchor | PageID |
+----+--------+--------+
| 1 | NULL | 2 |
+----+--------+--------+
I'll add some extra limitations here. IF the whole point of this is that it's providing a standardised way for handling links across the board in Silverstripe CMS, then it should provide a TinyMCE plugin that overrides the ones currently provided by admin/cms/asset-admin which creates/uses Link records in the DB. It should also provide reports for orphaned and for broken Link records It should probably also provide either a BuildTask or a button in the above-mentioned report for deleting orphaned links.
Broken links / TinyMCE are just making shortcodes in a Content
field though right? They're not the same thing as the Link Dataobjects that this module does?
That said maybe a leftfield idea here is to save everything in linkfield as shortcodes instead of DB Objects, and require any custom Link subclasses to define their own shortcodes
Broken links / TinyMCE are just making shortcodes in a Content field though right? They're not the same thing as the Link Dataobjects that this module does?
Broken links are not shortcodes so I don't know what you meant by that part.
TinyMCE links are stored in the DBHtmlField as shortcodes, and I would recommend that happen with an implementation that this module provides, as well. The shortcode would look something like [link,id=1]
, and the shortcode parser would get the associated link record and render its template. You would then have a standardised model which handles links everywhere, whether it's in the WYSIWYG or in a GridField or in the AnyRelationField or whatever.
That said maybe a leftfield idea here is to save everything in linkfield as shortcodes instead of DB Objects, and require any custom Link subclasses to define their own shortcodes
Shortcodes don't actually store data, really. Look at the "page on this site" shortcode for example: <a href="[sitetree_link,id=1]">my page</a>
all the shortcode itself stores is the ID for the page. The shortcode parser then fetches that page and replaces the shortcode with the URL for that page. You're still storing the URL in the SiteTree record. In the same way, I'm proposing we store all links in a Link record, and use shortcodes to say "this is where that link should go in the content"
Note that that would not be MVP - I'd recommend we look at doing that as an enhancement down the line rather than as part of the initial epic.
Broken links are not shortcodes so I don't know what you meant by that part.
Ah yup you're right, sorry confused myself. From memory the broken links uses some pretty old and fragile code that at least partially uses SiteTreeLinkTracking to generate the report.
It should also provide reports for orphaned and for broken Link records
OK so you're saying there should be a central way to quickly work out which of all the internal/external Link records are broken?
[link,id=1]
So you could "Insert a link" in TinyMCE and select an email address (which you can do now), though you're saying that link would now get saved as a new DataObject and a shortcode inserted into the Content field, instead of what currently happens where it just gets turned straight into <a title="My description" href="mailto:address@example.com?subject=My%20subject">My link text</a>
?
so you're saying there should be a central way to quickly work out which of all the internal/external Link records are broken?
Yeah. My understanding is that the whole point of this module is that there's a single standardised way to manage links in the CMS. The WYSIWYG should not be excluded from that goal IMO.
One very clear benefit being that a broken links report will "just work" - we wouldn't have to go through all HTML fields hunting for links, we can just do a Link::get()
. We could then choose to remove the SiteTreeLinkTracking
logic and change it to look for Link records - but we could also just add an edit link/button to the broken links report that opens the link modal, and fix the broken link right there without caring what page it's on.
Another advantage is that it makes it really easy to modify the way links display in a standardised way (e.g. adding an "external link" icon to all external links would be as simple as editing the link template).
So you could "Insert a link" in TinyMCE and select an email address (which you can do now), though you're saying that link would now get saved as a new DataObject and a shortcode inserted into the Content field
Yup that's exactly what I'm proposing.
I've created another issue here https://github.com/silverstripe/silverstripe-linkfield/issues/84 with a bunch of linked draft PRs that implement a bunch (though not all) of what I've discussed on this issue plus more
Link::canView() + other can*() methods are missing - though this doesn't really seem like an issue .. since they're saved as as part of page save process they essentially get just get the same permissions as the page they're on
Every DataObject should have proper permission check out of the box. Whether we tie the permissions to an explicit owner for the link or to some pre-existing CMS permission, I'm not sure.
But I don't consider the current status quo acceptable.
RequiredFields validation doesn't work Link as Required Field doesn't appear to work #69
We should aim to have more than just a black and white validation. The LinkField is designed to be extensible with the expectation that you could provide more complex Link Type. e.g. The external link should have validation to make sure that the provided URL is a valid URL.
The same way we use form schema to retrieve the form list, we could post back the data we got to see if it's valid.
Do we want to versioned this by default? This introduces a dependency on versioned.
Would we consider conditionally versioning link only if versioned is installed?
There's also a related concern that if you are versioning Links, you need to make sure they get published when the parent record is published.
Any field has some pretty solid support for "Has many" list. If we don't want to go all the way and jump on the AnyField band wagon right away, we can just copy its implementation.
I agree that ManyMany support is a bad idea. Links should be strongly tied to a parent ... the same way that an Elemental block is tightly coupled to an Elemental Area.
Add ability to specify link types at a per field level, rather than just globally
AnyField has that capability. Once you dump the registry idea and the need to register link classes up front, this becomes pretty easy.
Make it look/behave more like TinyMCE plugin
I like this idea. I could image a link shortcode that gets managed in a similar way to a link field.
However, this sounds more like a nice to have than something we should aim to support right away.
ORM/DBJson.php / ORM/DBLink.php and friends will be deleted - see notes below in "Notes about the 2x implementations"
Agree
AjaxField/FormFactoryExtension/LeftAndMain/ModalController
ModalController is equivalent to RemoteFileModalExtension
The LeftAndMain extension is just working around the fact that there's no "LinkAdmin" controller to host some endpoints. Asset-admin doesn't have that problem because it has its own controller.
FormFactory is something @mfendeksilverstripe created: https://github.com/silverstripe/silverstripe-linkfield/pull/65 . Not sure why he didn't directly update FormFactory.
AjaxField is working around a limitation of how some ajax field work (e.g. TreeDropDown). Those fields need a REST endpoints to fetch data from the CMS. That REST endpoint is accessed via their parent form. However, in the case of linkfield, the parent form depends on your Link type key ... if you don't know what kind of link you are editing, ModalController can't retrive the form fields.
In hindsight, AjaxField works OKish, but it's probably worth investigating some alternatives ... especially if we give ourselves permission to change things in core to accomodate it. Of the top of my head, I'm thinking we could rewrite route that fetches the link form so the link key is a URL parameter instead of a GET parameter. Alternatively, we could create versions of those form fields that call generic end points that are not tied to a form.
The Registry class and _config/types.yml is used to define the link types, though it's unnecessary we can just look for subclasses of Link::class. _config/types.yml does allow you to se enabled: false to globally disable link types, though a better way would be to just have a private static $enabled on Link and just config it off on the subclasses you don't want. Also I'd much rather just have LinkField.php::setAllowTypes()/setDenyTypes() methods directly on the LinkField to specific allowed link types in a given context
All this part can go if we get rid of the registry and assume that Link subclass can be managed by a LinkField. We should add an option to allow specific subclasses to be disabled in specific LinkField instances.
FileLinkModal.js calls the INIT_FORM_SCHEMA_STACK action which presumably does something in asset-admin
The file link form modal is a hack. I'm piggy backing on the File Link Form modal in the WYSIWIG. This means that you can't customised the file link form to add more information.
There's probably a way to do this more cleanly if you update core. I think there could be some really nice side benefit here. For example, you could give developers the ability to use their own form schema in an Insert Media Modal. I would like to do something like that in AnyField.
I disagree that GraphQL is substantially more complex than a REST here.
I think there's an argument that I'm subverting the main value GraphQL by JSON serializing the Link Form data and sending that to a GraphQL service to generate a description for it.
If we're going to ditch the AnyField idea, maybe we should consider making the entire thing work with GraphQL, including the link creation/update/delete. Only the ID of the created link would be attached to the form field. That would allow us to remove a bunch of logic from the LinkField PHP because the Link creation logic would be handled by GraphQL instead. (AnyField couldn't work that way without forcing you to pre-register all your DataObject in your admin schema.)
If we go REST, then I would like us to come up with a comprehensive convention of how and when we add REST endpoints to the CMS. Also, how we interact with those endpoints in React.
IF the whole point of this is that it's providing a standardised way for handling links across the board in Silverstripe CMS, then it should provide a TinyMCE plugin that overrides the ones currently provided by admin/cms/asset-admin which creates/uses Link records in the DB.
I don't think this needs to be a "be-all and end-all" link management solution. People have a need to manage links outside the WYSIWYG. That's basically the problem the other link module were trying to solve. To the extent that we could combine the two to provide a seamless experience, that might be worth exploring, but I don't see it as a outright blocker.
It should also provide reports for orphaned and for broken Link records. It should probably also provide either a BuildTask or a button in the above-mentioned report for deleting orphaned links.
Broken link tracking would be a plus.
On the orphan link tracking, I'm tempted to put it back on the developers to define cascade delete and ownership rules. But not 100% sure about this.
There's a couple overarching assumption that I made that probably should be made explicit.
If we are not comfortable with those assumptions, we should call it out.
Link can't exists by themselves and should be owned by a parent objects
There's not a whole lot we can do to stop that from happening since people can just programatically create DataObjects, or add them via a GridField. Only way to stop it is to get has_one implementation and forced all links to be has_many which means they'll have a ParentID (page) which we could validate beforeWrite(). I don't think there is any practical need to do this though
Link objects will be flat (no complex relations)
They're not flat now, SiteTreeLink and FileLink both have relations on them to SiteTree and File respectively
Developer can choose to create their own link types by subclassing Link and the LinkField can accommodate that.
That's already already doable
Developers can chose to create their own LinkModal for custom link types that will allow them to handle unusual scenarios.
I'm not sure what this means. If you create a new Link type you'll automatically get a dynamic form schema created for you inside the modal. Or do you mean using the js injector to swap out the modal itself? Not sure if that's possible now, though seems unnecessary / out of scope for what we're doing here if it's not.
I disagree that GraphQL is substantially more complex than a REST here.
GraphQL is lots more cluttered/complex for no benefit - I did a quick PR to POC getting rid of graphql and you end up removing a bunch of files and get functionally the same thing. To me that's seems like classic refactoring.
If we go REST, then I would like us to come up with a comprehensive convention of how and when we add REST endpoints to the CMS. Also, how we interact with those endpoints in React.
Convention I had in my PR was just using /admin/linkfield/<action>
and use the standard fetch global function.
Multiple links
Seems like you've already implemented that in this PR. I'm happy to just merge this as is and then we can do a tidy up PR as we looks at doing other refactoring.
Do we want to versioned this by default? This introduces a dependency on versioned.
I'd say so since the main use case here is to put them on pages, which are versioned.
Would we consider conditionally versioning link only if versioned is installed?
We could, though I'm not sure in what the scenario someone would be using the linkfield module and not have versioned installed
There's also a related concern that if you are versioning Links, you need to make sure they get published when the parent record is published.
Just update the usage documentation to include $own, $cascade_deletes and $cascade_duplicates when adding Link::class to a relationship on your Page/DataObject
e.g. The external link should have validation to make sure that the provided URL is a valid URL.
I've got that implemented on a PR for validation
Whether we tie the permissions to an explicit owner for the link or to some pre-existing CMS permission, I'm not sure.
Does "tie the permissions to an explicit owner for the link" mean just use the owner Page for the permission check? That's functionally what happens now. I've added canView() checks for objects for related data in this PR.
"or to some pre-existing CMS permission" - I'm not sure what this means. We wouldn't be using the Permission::check() API here
We discuss this at our architecture catch up yesterday.
The key take away are:
The LinkField never went through a full peer review process.
Acceptance criteria
Related