Open Nicholas-Westby opened 8 years ago
I think the difficult part here is determining a proper format for specifying the package actions, since it might be a bit complex.
grunt-umbraco-package
internally uses js2xmlparser
for transforming the manifest from JSON to XML. With a few lines of code added to grunt-umbraco-package
, we can use the same syntax to specify the package actions.
Below an example of two different package actions:
umbracoPackage: {
release: {
src: 'files/',
dest: './',
options: {
name: pkg.name,
version: version,
url: pkg.url,
license: pkg.license.name,
licenseUrl: pkg.license.url,
author: pkg.author.name,
authorUrl: pkg.author.url,
readme: pkg.readme,
outputName: pkg.name + '.v' + version + '.zip',
actions: [
{
'@': {
runat: 'uninstall',
undo: 'true',
alias: 'UmbracoFileSystemProviders.Azure.TransformConfig',
file: '~/web.config',
xdtfile: '~/App_Plugins/UmbracoFileSystemProviders/Azure/Install/web.config'
}
},
{
'@': {
runat: 'install',
undo: 'true',
alias: 'addDashboardSection',
dashboardAlias: 'MyDashboard'
},
section: {
areas: {
area: 'content'
},
tab: {
'@': {
caption: 'My tab'
},
control: '/App_Plugins/MyDashboard/MyTab.html'
}
}
}
]
}
}
}
Properties of the @
object represents XML attributes, while other properties represents child XML elements.
The first action would generate the following XML:
<Action runat="uninstall" undo="true" alias="UmbracoFileSystemProviders.Azure.TransformConfig" file="~/web.config" xdtfile="~/App_Plug
ins/UmbracoFileSystemProviders/Azure/Install/web.config"/>
The second action would generate the following XML:
<Action runat="install" undo="true" alias="addDashboardSection" dashboardAlias="MyDashboard">
<section>
<areas>
<area>content</area>
</areas>
<tab caption="My tab">
<control>/App_Plugins/MyDashboard/MyTab.html</control>
</tab>
</section>
</Action>
@tomfulton What do you say about this? If the syntax seems fine and not overly complex, I can make a pull request to add support for this ;)
That would work for me, but might be a bit overly complicated (unless I'm missing something, which I very well might). Currently, the readme can be specified with a string value. Why not do the same for package actions? The native "fs" Node.js module could be used to read in the contents of an XML file, then that could be passed to actions property.
umbracoPackage: {
release: {
src: 'files/',
dest: './',
options: {
name: pkg.name,
version: version,
url: pkg.url,
license: pkg.license.name,
licenseUrl: pkg.license.url,
author: pkg.author.name,
authorUrl: pkg.author.url,
readme: pkg.readme,
outputName: pkg.name + '.v' + version + '.zip',
actions: fs.readFileSync("sample.xml", {encoding: "utf8"});
}
}
}
Does js2xmlparser allow you to inject a string as XML (i.e., rather than having to specify the entire hierarchy in JSON)?
It seems like the most common scenario will be that package actions will start as XML. Any conversion to JSON will probably be a manual process (i.e., the developer would be converting from XML to JSON so that js2xmlparser could then convert it back to XML).
Hi @Nicholas-Westby, thanks for reporting! I've been wondering how we might achieve the same for Document Types and the like, and wonder if this might be the time to tie all of these in.
Before getting too far, I should mention that what you're after is possible now, by using your own package.xml
with the actions predefined, instead of letting the task generate it for you. You can do this by specifying the manifest
option - check these for an example: Gruntfile.js, package.xml. I'll get the README updated to explain this better.
Regarding supporting this with the package.xml generation method though, @abjerner echoed my thoughts exactly - the trick is figuring out a format to specify them that makes sense. js2xmlparser
seems pretty powerful, but yeah, the OOTB configuration seems a little verbose.
I think your idea of specifying an XML string for each of the "entities" packages support might be a good option. It does seem a bit strange though, since we're really trying to automate the XML generation with the latest update. But, with the exception of Actions, I'm guessing most users would use the Umbraco Backoffice to manually generate the XML first anyway, in which case maybe it makes sense to just let them copy/paste it...
@abjerner regarding your suggestion, I think something like this would be good for Actions
(ideally if we could remove that second level '@': { ... }
for each item though). But, I'm not sure this is the right solution for the other entities that Packages support, like DictionaryItems
, Documents
, etc etc, and maybe it makes sense to use the same solution for all? A quick look at the XSLTSearch package.xml
makes me think that this approach could get messy quick:
<Documents>
<DocumentSet importMode="root">
<XSLTsearch id="1090" parentID="-1" level="1" writerID="0" creatorID="0" nodeType="1087" template="1086" sortOrder="39" createDate="2010-11-09T13:45:22" updateDate="2010-11-09T14:18:04" nodeName="Search" urlName="search" writerName="Administrator" creatorName="Administrator" path="-1,1090" isDoc="">
<umbracoNaviHide>0</umbracoNaviHide>
</XSLTsearch>
</DocumentSet>
</Documents>
<DocumentTypes>
<DocumentType>
<Info>
<Name>XSLTsearch</Name>
<Alias>XSLTsearch</Alias>
<Icon>.sprTreeDoc2</Icon>
<Thumbnail>doc.png</Thumbnail>
<Description>XSLTsearch page.
(adjust settings via the macro in the XSLTsearch template)</Description>
<AllowedTemplates>
<Template>XSLTsearch</Template>
</AllowedTemplates>
<DefaultTemplate>XSLTsearch</DefaultTemplate>
</Info>
<Structure />
<GenericProperties>
<GenericProperty>
<Name>Hide page?</Name>
<Alias>umbracoNaviHide</Alias>
<Type>38b352c1-e9f8-4fd8-9324-9a2eab06d97a</Type>
<Definition>92897bc6-a5f3-4ffe-ae27-f2e7e33dda49</Definition>
<Tab>
</Tab>
<Mandatory>False</Mandatory>
<Validation>
</Validation>
<Description><![CDATA[]]></Description>
</GenericProperty>
</GenericProperties>
<Tabs />
</DocumentType>
</DocumentTypes>
It also seems that some entities use different casing, for example, the Macro elements are all camelCased. We could probably overcome all of this with some configuration of js2xmlparser
, but I wonder if it'd be easier for the developer to just use the XML instead of manually converting it to JSON?
What do you think about Nicholas' suggestion of using strings? Something like this?
umbracoPackage: {
release: {
src: 'files/',
dest: './',
options: {
name: pkg.name,
version: version,
url: pkg.url,
license: pkg.license.name,
licenseUrl: pkg.license.url,
author: pkg.author.name,
authorUrl: pkg.author.url,
readme: pkg.readme,
outputName: pkg.name + '.v' + version + '.zip',
actions: grunt.file.read('/config/package/actions.xml'),
documents: grunt.file.read('/config/package/documents.xml'),
dictionaryItems: grunt.file.read('/config/package/dictionaryItems.xml'),
/// ... etc
}
}
}
Or if we want to keep things even simpler, maybe one field for all the XML?
options: {
name: pkg.name,
version: version,
url: pkg.url,
license: pkg.license.name,
licenseUrl: pkg.license.url,
author: pkg.author.name,
authorUrl: pkg.author.url,
readme: pkg.readme,
additionalXml: grunt.file.read('/config/package/umbracoPackageXml.xml') // Any <Actions>, <DocumentTypes>, etc
}
I'm open to either approach (or other ideas), curious to hear what you all think makes sense! :+1:
We can check the type of options.actions
when running the task, making it up to the developer what approach to use:
JSON
If the type is an array, it could work similar to what I posted earlier - the specified package actions are added to the JSON object representing the manifest before it is converted to XML. If we are to specify the package actions with JSON, I don't really think we can do much about the format when still using js2xmlparser
, since the XML is in a format (attributes and child elements) unknown to the task. So we could call this the advanced approach.
For anything other than package actions, we can use a simpler JSON format like the example below, and then handle the rest in the task (proper casing of the XML elements and similar):
documentTypes: [
info: {
name: 'XSLTsearch',
alias: 'XSLTsearch',
icon: '.sprTreeDoc2',
thumbnail: 'doc.png',
description: 'XSLTsearch page. (adjust settings via the macro in the XSLTsearch template)',
allowedTemplates: ['XSLTsearch'],
defaultTemplate: 'XSLTsearch'
},
genericProperties: [
{
name: 'Hide page?',
alias: 'umbracoNaviHide',
type: '38b352c1-e9f8-4fd8-9324-9a2eab06d97a',
definition: '92897bc6-a5f3-4ffe-ae27-f2e7e33dda49',
mandatory: false
}
]
]
The above is just a quick example. I have omitted a few fields here and there, but the task could just fill it the blanks. Again this could be considered the advanced approach.
XML
If the type is a string (XML), it becomes a little tricky to parse the XML into JSON, and then back to XML again.
However when the manifest is converted to an XML string, we can do a simple string replacement by replacing <Actions/>
with the XML string specified in the options. This will mess up the indentation a bit, but otherwise seems to work fine. Then this would just work like @Nicholas-Westby suggested in his example. So this could be the simple approach.
Changes to the task The change to allow package actions being specified with JSON just takes three lines of code at the right location:
if (Array.isArray(options.actions) && options.actions.length > 0) {
data.Actions.Action = options.actions;
}
The code necessary is also very straight forward (and can be used for document types and similar as well). Currently we're just saving the generated XML directly to the package.xml
file (see here), but if we store the XML in a variable first, the code for the string replacement could look like:
if (typeof(options.actions) == 'string') {
if (options.actions.indexOf('<Actions') >= 0) {
xmlManifest = xmlManifest.replace('<Actions/>', options.actions);
}
}
and for document types like:
if (typeof(options.documentTypes) == 'string') {
if (options.documentTypes.indexOf('<DocumentTypes') >= 0) {
xmlManifest = xmlManifest.replace('<DocumentTypes/>', options.documentTypes);
}
}
I hope this makes sense. Otherwise I can make a pull request, and you can play around with it ;)
@tomfulton Perfect! I went ahead and just used a custom manifest and it seems to be working exactly as I'd hope: https://github.com/rhythmagency/formulate/commit/1402b54cee4172da86166156c0c7d3f860c5cc7d
Some clarification in the documentation would definitely go a long way to help developers understand that this is already possible.
Hey @ajberner - Great idea, I love the approach of allowing the new option(s) to accept strings or JSON, and just injecting the XML with a string replacement.
I also like your thought about using the simpler syntax for the JSON (except for Actions). At first thought it does seem like this may be a lot of work though - handling every attribute a package can have, especially when I see things like this and think about what it looks like with more/nested content:
<Documents>
<DocumentSet importMode="root">
<XSLTsearch id="1090" parentID="-1" level="1" writerID="0" creatorID="0" nodeType="1087" template="1086" sortOrder="39" createDate="2010-11-09T13:45:22" updateDate="2010-11-09T14:18:04" nodeName="Search" urlName="search" writerName="Administrator" creatorName="Administrator" path="-1,1090" isDoc="">
<umbracoNaviHide>0</umbracoNaviHide>
</XSLTsearch>
</DocumentSet>
</Documents>
...but maybe you already have an idea of how to handle this. Personally I think I would use the "simple"/XML approach if I had a complex package.xml
to work with. But I'm happy to support the JSON method too if it'd be useful for you. If the simple syntax is too much work, I would be fine using your original suggestion of the js2xmlparser
syntax too.
Lastly, I wonder if it makes sense to bundle all of these into one option to help keep the # of options manageable. For something like XSLTSearch, I imagine it might be cleaner to point something like optionsXml
to a single file containing all of the entities, rather than having to split into 5 different files. Not a big deal, just thinking out loud here :)
Anyway, if you're bored I would gladly accept a PR, otherwise I will look to get this added over the next couple weeks.
Thanks! Tom
One one of my projects, I recently added a package action that gives the user installing the package permission to see the section added by that package: https://github.com/rhythmagency/formulate/commit/ae2095389f0713c7db4efd8aa88757f4ab96c1e1
I was thinking of using grunt-umbraco-package to create the Umbraco package so I don't have to do it manually every time I create a release. However, I didn't see any documentation that indicated it was possible to add package actions with this tool, so I'll have to go the manual route for now.
Would be awesome if you added support for package actions to be included.