As a developer, I can "add" or "override" admin bar items, piece filters, batch operations, single-piece operations, etc. as elegantly as fields
See original GitHub issueThe problem
When writing a new module that extends another module, merging in new schema fields is really easy in 3.x. But merging in other kinds of extensible things is as hard as ever.
What we did for schema fields
The new 3.x module format has a really nice pattern for adding and overriding schema fields, via special section in the module format:
module.exports = {
extend: '@apostrophecms/piece-type',
fields: {
add: {
color: {
type: 'select',
choices: [ ... ]
}
},
remove: [ 'published' ]
}
}
If other fields were inherited from a base class, it just merges: if a field has the same name it is replaced, otherwise it is added.
remove
also behaves sensibly. If a subclass removes a field it’s gone; if a sub-subclass removes a second field, both fields are gone; if a sub-sub-subclass adds one of the fields back, it’s back… feels good.
You can even use a function for fields
if you need to peep at options
when deciding what fields to include.
What we used to have to do for fields (and still have to do for other things)
This replaced a pattern that was hard to get right in an extensible way:
// 2.x
module.exports = {
beforeConstruct(self, options) {
options.addFields = [
{
name: 'color',
type: 'select',
choices: [ ... ]
}
].concat(options.addFields || []);
}
};
If you didn’t do this special “beforeConstruct concat dance,” your addFields
would cancel out the one in the base class or the one from your subclass. Bad news.
Examples of things we didn’t provide a solution for
- Modules extending
piece-type
can havefilters
. There is a defaultfilters
option with the core stuff likepublished
, and anaddFilters
option to avoid accidentally removing those. But this doesn’t help you write a module that can be subclassed with even more, unless you follow the “concat pattern” above. - Modules extending
piece-type
can havebatchOperations
. Same concern. - Modules extending
piece-type
can have custom operations on individual pieces too. There’s no provision for this in 3.x right now. In 2.x the pattern was a one-off. - There’s more:
addImageSizes
,addCsrfExceptions
,addColumns
. Probably others that didn’t follow this pattern but could have.
All of these have the same problem as addFields
, but no improved solution in 3.x. They also all have a name
, which suggests they can be solved the same way.
Proposed solution
- All of these things, not just fields, should have sections in the module format. Let’s call them “cascades” for now.
- They should work just like fields, i.e. with
add
,remove
andgroup
subsections. Althoughgroup
is not necessary for all of them, there could be others that benefit from grouping. - We should not hard-code a list of these sections in apostrophe core. Instead, a module should be able to register one. Note that this would usually be done once by us in a core module that other people are extending, but not necessarily:
// in modules/@apostrophecms/doc-type/index.js
module.exports = {
cascades: [ 'fields' ]
}
// in modules/@apostrophecms/piece-type/index.js
module.exports = {
extend: '@apostrophecms/doc-type',
cascades: [ 'filters', 'columns', 'operations', 'batchOperations' ]
// Set up the default batch operations, etc.
batchOperations: {
add: {
trash: {
label: 'Trash'
}
}
}
}
// in modules/@apostrophecms/attachment/index.js
module.exports = {
cascades: [ 'imageSizes' ],
imageSizes: {
add: {
// standard image sizes are born here
'one-half': { ... },
full: { ... }
}
}
}
Now here’s a project level modules/@apostrophecms/attachment/index.js
tweaking the image sizes:
// in modules/@apostrophecms/attachment/index.js
module.exports = {
imageSizes: {
add: {
'epic': { width: 5000, height: 5000 }
},
remove: [ 'one-half' ]
}
}
And a custom piece type (might be project level, might be npm, whatever) that adds a “manage” filter:
// In products/index.js
module.exports = {
extend: '@apostrophecms/piece-type',
filters: {
add: {
color: {
label: 'Color',
multiple: true
}
}
}
}
Notice that I do NOT have to re-declare
cascades
in project level stuff, or even in any pieces module that is just extending cascades that were already declared in the base class. Once it’s turned on, the subclasses get it for free.
What comes out the other end?
An important question is what the module sees at the end of this process.
Right now, we turn fields
back into options.addFields
and that gets turned into an array as self.schema
. Basically… basically so I wouldn’t have to rewrite the schemas module. No better reason.
I think that was a mistake. I think fields
should get turned into self.fields
, which should be an object. batchOperations
would get turned into self.batchOperations
, which would be an object. Etc.
This doesn’t mean modules can’t turn that object into an array at the end if they want to, it’s just not done by the module format processor. What comes out should be an object.
(Note that the order of object properties is well defined in modern JavaScript, including Node.js and IE11+ browsers. At one point it was not.)
What do you think folks?
cc @stuartromanek @breyell @localghost443 @abea @livetodeliver @bgantick @bobclewell @ngranahan @falkodev
Issue Analytics
- State:
- Created 3 years ago
- Reactions:1
- Comments:5 (5 by maintainers)
Top GitHub Comments
This sounds right to me. To make sure I understand,
cascades
would be declared in these base (and a few other) core modules, so normally it would never be used in projects? Maybe someone doing something really fancy would want a new cascading option, in which case they could add thatcascades
option with only the new option in it?This was merged to 3.0.