Skip navigation
All Places > Alfresco Content Services (ECM) > Blog > 2016 > October
2016

v1 REST APIs with Aikau

Posted by ddraper Oct 25, 2016

My colleague Gavin Cornwell has started writing a series of blog posts on how to make use of the v1 REST APIs that will be available in the next release of Alfresco. PLEASE NOTE: If you want to test out the example in this post then you'll need to follow the setup steps from Gavin's posts!

 

Aikau was originally written to work with the the internal WebScript based REST APIs written for Share. As these REST APIs were not intended for public consumption there is some inconsistency in both the parameters used for the requests and the schema of the response body. This meant that Aikau widgets were intentionally written to be easily configurable to work with different APIs and the use of the services meant that it was easy to normalise any data provided.

 

One of the many great things about Aikau is the fact that it will quite happily work with any of the REST APIs provided by the Alfresco Repository be they WebScripts, CMIS or even the new v1 APIs. This blog post provides a very simple example of how to access and render the data shown in Gavin's second post. The point of the post is to show how to provide a service that can work with the v1 APIs and how to widgets can be configured. At some point in the near future Aikau is likely to start providing dedicated services for working with the v1 APIs (in fact the FileUploadService already can be configured to work with them!). I've stripped the example back to the most basic rendering to focus on how to access the data - the usual principles of building Aikau pages can be applied to make a more interesting and useful interface.

 

Our custom service "blogs/V1RestApiDocumentService" looks like this:

 

define(["dojo/_base/declare",
        "alfresco/services/BaseService",
        "alfresco/core/CoreXhr",
        "alfresco/core/topics",
        "service/constants/Default",
        "dojo/_base/lang"], 
        function(declare, BaseService, CoreXhr, topics, AlfConstants, lang) {
   
   return declare([BaseService, CoreXhr], {

      registerSubscriptions: function blog_V1RestApiDocumentService_registerSubscriptions() {
         this.alfSubscribe(topics.GET_DOCUMENT_LIST, lang.hitch(this, this.onRetrieveDocumentsRequest));
      },

      onRetrieveDocumentsRequest: function blog_V1RestApiDocumentService_onRetrieveDocumentsRequest(payload) {
         var url =  AlfConstants.URL_CONTEXT + "proxy/alfresco-api/-default-/public/alfresco/versions/1/nodes/-root-/children";
         var config = {
            alfSuccessTopic: payload.alfSuccessTopic,
            alfFailureTopic: payload.alfFailureTopic,
            url: url,
            method: "GET",
            callbackScope: this
         };
         this.serviceXhr(config);
      
   });
});

 

The service comprises of two functions registerSubscriptions (which all services should implement) to create the subscription to the topics that the service should respond to - in this case the standard topic for getting documents. The subscription creates the binding to the onRetrieveDocumentsRequest function that actually makes the XHR request for the data.

 

The key thing to note here is how the url variable is constructed - by appending "/proxy/alfresco-api" to the application context (i.e. "/share") then you get authenticated access to the v1 REST APIs (in much the same way that "/proxy/alfresco" will get authenticated access to the WebScript REST APIs). The remainder of the URL (from "-default-" onward) is taken from the example in the blog post.

 

Our custom service mixes in the CoreXhr module to gain access to the serviceXhr function that is called to make the XHR request. The alfSuccessTopic and alfFailureTopic attributes are expected to be provided (which will be handled by the defaultSuccessCallback and defaultFailureCallback functions respectively provided by the CoreXhr module).

 

A simple page model to use this service and render the results could look like this:

 

model.jsonModel = {
   services: ["blog/V1RestApiDocumentService"],
   widgets: [
      {
         name: "alfresco/lists/AlfList",
         config: {
            itemsProperty: "list.entries",
            metadataProperty: "list.pagination",
            startIndexProperty: "list.pagination.skipCount",
            totalResultsProperty: "list.pagination.totalItems",
            widgets: [
               {
                  name: "alfresco/lists/views/AlfListView",
                  config: {
                     widgets: [
                        {
                           name: "alfresco/lists/views/layouts/Row",
                           config: {
                              widgets: [
                                 {
                                    name: "alfresco/lists/views/layouts/Cell",
                                    config: {
                                       widgets: [
                                          {
                                             name: "alfresco/renderers/Property",
                                             config: {
                                                propertyToRender: "entry.name"
                                             }
                                          }
                                       ]
                                    }
                                 }
                              ]
                           }
                        }
                     ]
                  }
               }
            ]
         }
      }
   ]
};

 

It is the AlfList that will make the request to the service - but in order to be able to work with the v1 REST API schema it is necessary to reconfigure:

  • the itemsProperty - to indicate where the array of items can be found in the response
  • the metadataProperty - to indicate where information about the items can be found
  • the startIndexProperty -  for pagination data handling (not used in this example) - to indicate what page
  • the totalResultsProperty - also for pagination data handling

 

Unfortunately we're not able to reuse any of the existing Document Library views (such as AlfSimpleView, AlfDetailedView, etc) because the properties in the v1 REST API response schema are different. So here we're just constructing a simple view to render the name of each node. More information on build views for lists can be found in the Aikau tutorial on GitHub.

 

The end result is not especially exciting, it looks like this:

 

Aikau-V1-REST-API-1.png

... but you should hopefully recognize the rendered values as the names of the folders that you'd find in the alfresco://company/home in the Alfresco Repository.

 

The key thing to takeaway from this post is how easy it is to setup up Aikau to work with the new APIs. In future releases of Aikau we will provide services for working directly with the new APIs, but this should hopefully demonstrate that you can easily write your own services to use them and still make full use of the hundreds of widgets that are currently available in Aikau.

 

Update 26/05/2016

I've done some more experimenting with the V1 REST APIs and made a better user interface (see video below). This video demonstrates some widgets from a development branch of Aikau that has (at the time of writing) yet to be merged into the main project. This branch contains a number of performance improvements as well as a set of widgets with a Material Design Lite skin. You should note that the majority of the existing capabilities of Aikau lists are being re-used and that the interface is driving the browsing, pagination and sorting capabilities of the v1 REST API.

 

In the last post we looked at how to navigate the repository, this time we're going to create some files and folders.

 

As before all of the endpoints we'll cover in this blog post have been provided in a Postman collection and can be imported by clicking the "Run in Postman" button below.

 

button.svg

 

Let's start with creating a folder in our home folder. We use the same URL as we did for navigating the repository but this time we'll POST to it and use the -my- alias.

 

To create a folder named "My Folder" POST the body below using a Content-Type of application/json to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children

 

{
  "name":"My Folder",
  "nodeType":"cm:folder"
}

 

This results in a response representing the newly created folder as shown below:

 

{
  "entry": {
    "aspectNames": [
      "cm:auditable"
    ],
    "createdAt": "2016-10-17T18:30:28.870+0000",
    "isFolder": true,
    "isFile": false,
    "createdByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "modifiedAt": "2016-10-17T18:30:28.870+0000",
    "modifiedByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "name": "My Folder",
    "id": "de8f6834-1d5a-4137-ab1f-67e6978f1aa8",
    "nodeType": "cm:folder",
    "parentId": "bd8f1283-3e84-4585-aafc-12da26db760f"
  }
}

 

You may have noticed that slightly more information about the node (aspectNames and properties) is returned by default but even here we are still using a "performance first" principle. The include parameter can also be used with create, see http://localhost:8080/api-explorer/#!/nodes/addNode for the list of extra information you can request.

 

Let's now create an empty file within our home folder. Once again we'll POST to http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children but this time we'll send the following body:

 

{
  "name": "my-file.txt",
  "nodeType": "cm:content"
}

 

This results in a response representing the newly created document as shown below:

 

{
  "entry": {
    "aspectNames": [
      "cm:auditable"
    ],
    "createdAt": "2016-10-17T18:58:38.717+0000",
    "isFolder": false,
    "isFile": true,
    "createdByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "modifiedAt": "2016-10-17T18:58:38.717+0000",
    "modifiedByUser": {
      "id": "test",
      "displayName": "Test Test"
    },
    "name": "my-file.txt",
    "id": "e88e9b0f-7975-4398-beb4-db593b0b7aee",
    "nodeType": "cm:content",
    "content": {
      "mimeType": "text/plain",
      "mimeTypeName": "Plain Text",
      "sizeInBytes": 0,
      "encoding": "UTF-8"
    },
    "parentId": "bd8f1283-3e84-4585-aafc-12da26db760f"
  }
}

 

As well as accepting JSON the same endpoint also accepts multipart/form-data, allowing us to upload content from a standard HTML form or from the command line using curl.

 

Presuming there is a file named test.txt in the current directory try the following command:

 

curl -X POST -H "Authorization: Basic dGVzdDp0ZXN0" -H "Content-Type: multipart/form-data; boundary=----FormBoundary7MA4YWxkTrZu0gW" -F "filedata=@test.txt" "http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children"

 

Alternatively, the 3rd request in the Postman collection can be used after selecting a file in the "Body" tab as shown in the screenshot below.

 

Screen_Shot_2016-10-17_at_20_56_04.png

 

If you hadn't already noticed, one of the benefits of the API Explorer is that it allows you to "Try it out!" on all documented APIs. Unfortunately the OpenAPI specification does not currently allow multiple content types to be documented for the same endpoint. For POST /nodes/-my-/children we made the decision to focus the API Explorer on JSON to allow the creation of folders and files. We have however fully documented what is possible via multipart/form-data, we'll discuss a few of those options next.

 

To specify the name of the file that gets created you can use a form field called name as shown in the screenshot below. You can try this for yourself via the 4th request in the Postman collection.

 

Screen Shot 2016-10-18 at 20.04.27.png

 

When uploading content it's quite common for a file with the same name to exist, this will generate an error by default, to avoid the error the autoRename form field can be used as shown below. You can try this for yourself via the 5th request in the Postman collection. If a file name clash is detected a suffix will be added to the file name, for example my-file.txt will become my-file-1.txt.

 

Screen Shot 2016-10-18 at 20.25.36.png

 

The same thing can be achieved whilst creating folders and empty files via the same named query parameter, for example: http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-my-/children?autoRename=true

 

In some scenarios the path of the destination folder is known, so to avoid having to lookup the id of the folder a relativePath form field can be provided as shown in the screenshot below. You can try this for yourself via the 6th request in the Postman collection.

 

Screen Shot 2016-10-18 at 21.21.29.png

 

This will create a file named "my-file.txt" in the folder we created earlier i.e. in "/Company Home/User Homes/test/My Folder". The same thing can be achieved whilst creating folders and empty files by using the same named property in the body as follows:

 

{
   ...
   "relativePath": "/My Folder"
}

 

Another feature of the repository we can control when uploading content is the generation of a rendition. To have the thumbnail rendition used by Share generated, provide a renditions form field with a value of doclib as shown in the screenshot below. You can try this for yourself via the 7th request in the Postman collection.

 

Screen Shot 2016-10-18 at 21.43.36.png

Currently only one rendition can be requested, we plan to allow multiple in the future hence the plural form field name.

 

Finally, let's take a look at how we set properties. Any other form field will be presumed to represent a property to set. The screenshot below and the last request in the Postman collection shows how to set the cm:title, cm:description and exif:manufacturer properties. Properties have to follow the prefix:localname format and be a registered property via the content model otherwise they are silently ignored.

 

Screen Shot 2016-10-18 at 21.43.58.png

If we take a look at the response we'll see that the appropriate aspects have also been automatically applied.

 

{
  "entry": {
    ...
    "aspectNames": [
      "cm:versionable","cm:titled","cm:auditable","cm:author","exif:exif"
    ],
    "properties": {
      "cm:title": "The Title",
      "cm:versionType": "MAJOR",
      "cm:versionLabel": "1.0",
      "exif:manufacturer": "Canon",
      "cm:description":"The Description"
    }
  }
}

 

We've covered a lot of ground in this post but we still haven't looked at all the capabilities available; overwriting, versioning, custom types and associations will all be covered in future posts. Next time we'll be looking at how to retrieve, update and delete files & folders.

Introduction

Form controls in Aikau have always provided a rich and extensible way of validating the data that the user has provided. The fields in a form communicate over the publication/subscription layer to keep track of each others state and the form can only ever be submitted if all the fields report that they are in a valid state. Up until now it has only been able for a field to report that it is invalid in such a way that the form cannot be submitted. However from Aikau 1.0.91 onward it is now possible for a field to configure a validation state that acts purely as a warning.

 

Previous Validation Configuration

Because all Aikau form controls inherit from a common parent they are all able to make use of the same validation configuration. This is done via the validationConfig array. This array can be populated with one of more validations, each processed individually and a field will only report itself as valid if all conditions have been passed. A number of validation handlers are provided by the FormControlValidationMixin module, these are:

 

  • minLength (check that the value has at least a minimum number of characters)
  • maxLength (check that the value does not exceed a maximum number of characters)
  • regex (check that value matches a Regular Expression)
  • validateUnique (check that the identifier is unique by making an XHR request to the Alfresco Repository)
  • validateMatch (check that the value of the field matches that of another field)
  • validationTopic (validate the value by publishing the value to a service)

 

These validation handler are nothing more than functions provided by the FormControlValidationMixin module. This means that it is possible for form controls to provide specific validations that are only relevant for their particular type - for example the DateRange widget provides a validation called validateFromIsBeforeTo and Password provides a validation called confirmMatchingPassword. A custom form control can provide its own validation just by creating a function of that name - the function should accept a validationConfig argument and call the reportValidationResult function with the result, for example:

 

confirmMatchingPassword: function alfresco_forms_controls_Password__confirmMatchingPassword(validationConfig) {
   var isValid = this.confirmationTargetValue === this.getValue();
   this.reportValidationResult(validationConfig, isValid);
}

 

Each validation can accept custom attributes, but all validations it is possible to provide any validation with errorMessage and invertRule attributes that define the error message to display and invert the behaviour of that rule.

 

Example

This is a simple example that shows a TextBox that cannot have a value that is less than 3 characters or more than 5:

 

{
   name: "alfresco/forms/controls/TextBox",
   config: {
      label: "Example",
      description: "Between 3 and 5 chars",
      name: "name",
      value: "",
      validationConfig: [
         {
            validation: "minLength",
            length: 3,
            errorMessage: "Too short"
         },
         {
            validation: "maxLength",
            length: 5,
            errorMessage: "Too long"
         }
      ]
   }
}

 

Asynchronous Validation

The validateUnique and validationTopic are examples of asynchronous validations. They are not expected to return with the result immediately and the will move the form into the invalid state whilst awaiting the response. If the response takes more than a specific amount of time (configured through the _validationInProgressTimeout attribute on the form control) then an "in progress" indicator will be displayed.  The validationTopic validation has also been improved recently to support scoping of responses and configurable validation payloads. This has been done to support a forthcoming feature to perform asynchronous validation of site shortName and title when creating sites via the Aikau SiteService. You will find an example of asynchronous validation in the Search Manager page in Share 5.1 where facet names are validated for uniqueness.

 

Warnings Only

As of Aikau 1.0.91 it is now possible to use the warnOnly attribute to indicate that the validation message should be displayed but that the form submission button should not be disabled. This will be used in the forthcoming release of Share to indicate when a site title has already been used. It is still valid for site titles can be duplicated but it is helpful to alert users to duplicates. This is an example of configuring warnings from the Aikau unit test application:

 

{
   name: "alfresco/forms/controls/TextBox",
   config: {
      fieldId: "TOPIC_VALIDATION",
      label: "Customized validation Topic",
      description: "This simulates validating uniqueness of site identifier",
      value: "test",
      validationConfig: [
         {
            warnOnly: true,
            validation: "validationTopic",
            validationTopic: "ALF_VALIDATE_SITE_IDENTIFIER",
            validationValueProperty: "title",
            validationPayload: {
               title: null
            },
            validateInitialValue: false,
            negate: true,
            validationResultProperty: "response.used",
            errorMessage: "Identifier has been used"
         }
      ]
   }
}

 

The warnings look like this:

AikauValidation1.png

The first text box in the image is configured to validate as an error and the second is configured to validate only as a warning. Note that the validation indicator image is different as is the location and colour of the text. If a form has both a validation error and a validation warning then only the error indicator will be displayed and form submission will be disabled.

 

Summary

Hopefully this provides a useful refresher for Aikau form validation as well as the new features that have been recently added. If you have any questions about this then please ask them in the comments section below.

In Aikau 1.0.91 you'll find that there is an update to the autoSetConfig that could be applied to any form control. The ability to automatically set form control values based on changes to the value of other fields has always been present in Aikau, however it was only possible to configure specific values to set. For example you could do something like this:

 

{
   id: "SOURCE",
   name: "alfresco/forms/controls/Select", 
   config: {
      fieldId: "SOURCE_FIELD",
      label: "Choose from these...",
      name: "source",
      value: "",
      optionsConfig: {
         fixed: [
            { label: "One", value: "1"},
            { label: "Two", value: "2"},
            { label: "Three", value: "3"}
         ]
      }
   }
},
{
   id: "TARGET",
   name: "alfresco/forms/controls/TextBox",
   config: {
      name: "target",
      label: "...to set this",
      autoSetConfig: [
         {
            rulePassValue: "Option 3 Selected",
            ruleFailValue: "",
            rules: [
               {
                  targetId: "SOURCE_FIELD",
                  is: ["3"]
               }
            ]
         }
      ]
   }
}

 

In the above example the TARGET field would automatically have its value set to be "Option 3 Selected" whenever the user selected "Three" from the SOURCE field select menu. This facility was provided to enable hidden fields to be configured and is used effectively for this purpose by the InlineEditProperty renderer.

 

In the course of implementing the Inline Advanced Search we uncovered a use case where it was necessary for one field to automatically copy the value of another.  The reason why you might want to do this is in order to set the same value for multiple parameters. In the inline advanced search example there is the following model snippet:

 

{
   name: "alfresco/forms/controls/Select",
   config: {
      fieldId: "SELECT_FORM",
      label: "Look for",
      description: "This indicates the type of thing that you want to search for",
      name: "itemId",
      optionsConfig: {
         fixed: [
            {
               label: "Content", value: "cm:content"
            },
            {
               label: "Folder", value: "cm:folder"
            }
         ]
      }
   }
},
{
   name: "alfresco/forms/controls/TextBox",
   config: {
      fieldId: "DATA_TYPE",
      name: "formConfig.formSubmissionPayloadMixin.datatype",
      autoSetConfig: [
         {
            copyRule: {
               targetId: "SELECT_FORM"
            }
         }
      ],
      visibilityConfig: {
         initialValue: false
      }
   }
}

 

Here we have a hidden TextBox that is automatically updated to have the value of the advanced search form selected by the user. The autoSetConfig object now supports the copyRule attribute (which is only considered if rulePassValue or ruleFailValue are not provided). The targetId attribute within it indicates the field whose value should be copied.

 

The key difference between these two fields is the name attribute. The hidden field name attribute is targeting the autoSavePublishPayload value (see below). This is necessary in order to ensure that the "datatype" being searched for is included in the search payload.

 

autoSavePublishPayload: {
   itemKind: "type",
   mode: "edit",
   formId: "search",
   alfSuccessTopic: "ADV_SEARCH_FORM_RETRIEVED",
   formConfig: {
      okButtonLabel: "Search",
      okButtonPublishTopic: "ALF_ADVANCED_SEARCH",
      formSubmissionPayloadMixin: {
         alfResponseScope: "ADV_SEARCH_"
      },
      widgetsBefore: [
         {
            name: "alfresco/forms/controls/TextBox",
            config: {
               label: "Keywords",
               name: "searchTerm"
            }
         }
      ]
   }
}

 

This is a great example of how Aikau is able to enhance existing capabilities in a backwards compatible way. If you have any use cases that Aikau fails to address then please feel free to reach out to me either in the comments below, the discussion forums or as an issue on the Aikau GitHub repository.

Inline Advanced Search

Posted by ddraper Oct 20, 2016

Introduction

In a previous blog post I showed how Aikau was beginning to provide support for the XML-based Forms Runtime that Share uses to render forms using the old YUI2 controls. I then wrote additional blogs showing how it could be used to build Aikau pages for Tasks and Data Lists. I've recently looked into how the forms runtime is used to provide the Advanced Search page in Share and have added some updates into the 1.0.91 release to support a new Share extension that you can find here. This is provided as an extension module that was created using the approach described in this blog post.

 

PLEASE NOTE: In order to test this out you'll need at least aikau 1.0.91 JAR as well as the matching version of the aikau-forms-runtime-support JAR. These will need to be placed in your share/WEB-INF/lib folder (the server will need to be restarted).

 

How Advanced Search Works Currently

In order to perform an advanced search in Share it is necessary to go to the Advanced Search page. This allows you to select from a number of search forms configured in the Share forms runtime. When you select a form type ("Content" and "Folder" are the only two provided out-of-the-box) the form is loaded, shown on the page and you can then enter parameters that are relevant for searching for the data type matched to the form.

 

Inline Solution With Aikau

The sample extension provides a way in which advanced search could be used without switching pages. The purpose of this extension is just to show how you can make use of the Aikau FormsRuntimeService to work with existing form configuration. Let's step through the solution..

 

1. Include the FormsRuntimeService on the page

The FormsRuntimeService is not included in the page by default, so we need to add it into the array of services in the model. In the faceted-search.get.js controller extension this can be done as follows:

 

model.jsonModel.services.push("alfresco/services/FormsRuntimeService");

 

2. Add a button for Advanced Search

We're going to allow the user to initiate an advanced search by clicking a button (this isn't the only solution we could have applied, but it's very simple). We can use the "Developer Tools" (described in this blog post) to identify the widget to add the button to and to get the code snippet for finding that widget in the model.

 

var menubar = widgetUtils.findObject(model.jsonModel.widgets, "id", "FCTSRCH_TOP_MENU_BAR");
if (menubar && menubar.config && menubar.config.widgets)
{
   menubar.config.widgets.push(getAdvancedSearchButton());
}

 

3. Create the button definition

In the code snippet above you'll see that we're calling the getAdvancedSearchButton function - we now need to define this function to create a button.

 

function getAdvancedSearchButton() {
   return {
      name: "alfresco/buttons/AlfButton",
      config: {
         label: "Advanced Search",
         additionalCssClasses: "call-to-action"
      }
   };
}

 

4. Make the button request a dialog for the forms

At the moment the button isn't going to do anything because has not been configured with publishTopic or publishPayload attributes. We want to generate a dialog to show the advanced search forms in, so we need to publish on the relevant topic for creating a dialog. The basic configuration would look like this:

 

publishTopic: "ALF_CREATE_DIALOG_REQUEST",
publishPayload: {
   dialogId: "ADVANCED_SEARCH_DIALOG",
   dialogTitle: "Advanced Search",
   contentWidth: "1000px",
   hideTopic: "ALF_ADVANCED_SEARCH"
}

 

This defines the basic configuration for the dialog itself, but provides no information for what should go into it - for that we need to provide a widgetsContent attribute.

 

5. Add some dialog content

We need to be able to switch between the different advanced search forms. When you want Aikau to dynamically update a rendered model it is convenient to use the DynamicWidgets widget. This will update the model displayed based on publications on the topic it is configured to subscribe to:

 

widgetsContent: [
   {
      name: "alfresco/layout/DynamicWidgets",
      config: {
         subscribeGlobal: true,
         subscriptionTopic: "ADV_SEARCH_FORM_RETRIEVED"
      }
   }
]

 

6. Display an initial advanced search form

When the dialog is displayed we want to automatically display a default advanced search form. This can be configured through the publishOnShow attribute. We want to publish on the ALF_FORM_REQUEST topic (which the FormsRuntimeService subscribes to) and provide the necessary information for the form to be rendered. The main attributes are itemId, itemKind, mode and formId. In addition to this we need to override some of the default behaviour.

 

We need to make sure that the DynamicWidgets instance receives the form model to be rendered so the alfSuccessTopic is configured to match the subcriptionTopic previously used.

 

The default behaviour of a form generated by the FormsRuntimeService is to publish to the CrudService, so we need to override that via the formConfig attribute. Here we are changing the button label to be "Search" and the button publish topic to be ALF_ADVANCED_SEARCH (subscribed to by the AlfSearchList already on the page).

 

Interestingly the "Keywords" field that you see on the Advanced Search page is not configured as part of the form so we need to add it in using the widgetsBefore attribute.

 

publishOnShow: [
   {
      publishTopic: "ALF_FORM_REQUEST",
      publishPayload: {
         itemId: "cm:content",
         itemKind: "type",
         mode: "edit",
         formId: "search",
         alfSuccessTopic: "ADV_SEARCH_FORM_RETRIEVED",
         formConfig: {
            okButtonLabel: "Search",
            okButtonPublishTopic: "ALF_ADVANCED_SEARCH",
            formSubmissionPayloadMixin: {
               alfResponseScope: "ADV_SEARCH_",
               datatype: "cm:content"
            },
            widgetsBefore: [
               {
                  name: "alfresco/forms/controls/TextBox",
                  config: {
                     label: "Keywords",
                     name: "searchTerm"
                  }
               }
            ]
         }
      },
      publishGlobal: true
   }
]

 

7. Support advanced search form switching

This configuration would now render an advanced search form, but we want to enable the user to switch between any advanced search form that has been configured. To do this we're going to add an Aikau Form into the widgetsContent array to provide this.

 

Within the form we have a Select widget for choosing the form to display (hard-coded here to provide the out-of-the-box types). The form is configured to automatically save on changes using the autoSavePublishTopic (which publishes the ALF_FORM_REQUEST again) and the autoSavePublishPayload roughly matches that of the publishOnShow configuration previously used.

 

The purpose of the hidden TextBox that copies the selected form value is described in more detail here.

 

{
   name: "alfresco/forms/Form",
   config: {
      autoSavePublishTopic: "ALF_FORM_REQUEST",
      autoSavePublishGlobal: true,
      autoSavePublishPayload: {
         itemKind: "type",
         mode: "edit",
         formId: "search",
         alfSuccessTopic: "ADV_SEARCH_FORM_RETRIEVED",
         formConfig: {
            okButtonLabel: "Search",
            okButtonPublishTopic: "ALF_ADVANCED_SEARCH",
            formSubmissionPayloadMixin: {
               alfResponseScope: "ADV_SEARCH_"
            },
            widgetsBefore: [
               {
                  name: "alfresco/forms/controls/TextBox",
                  config: {
                     label: "Keywords",
                     name: "searchTerm"
                  }
               }
            ]
         }
      },
      widgets: [
         {
            name: "alfresco/forms/controls/Select",
            config: {
               fieldId: "SELECT_FORM",
               label: "Look for",
               description: "This indicates the type of thing that you want to search for",
               name: "itemId",
               optionsConfig: {
                  fixed: [
                     {
                        label: "Content", value: "cm:content"
                     },
                     {
                        label: "Folder", value: "cm:folder"
                     }
                  ]
               }
            }
         },
         {
            name: "alfresco/forms/controls/TextBox",
            config: {
               fieldId: "DATA_TYPE",
               name: "formConfig.formSubmissionPayloadMixin.datatype",
               autoSetConfig: [
                  {
                     copyRule: {
                        targetId: "SELECT_FORM"
                     }
                  }
               ],
               visibilityConfig: {
                  initialValue: false
               }
            }
         }
      ]
   }
}

 

The Final Result

The video below shows the advanced search in action. This is quite an involved use of the FormsRuntimeService but does show how it is beginning to support advanced customization - if you have any use cases that you'd like to see solved then please comment below.

 

Following the v1 REST API - Part 1 - Introduction to the v1 REST API in my last post it's now time to dive in and start using the API.

 

Whilst the v1 REST API has had a /nodes endpoint since it's introduction in 4.2 it could not be used to navigate the repository. One of the new endpoints added in the 5.2 EA Community releases is /nodes/{id}/children which allows us to retrieve the child nodes of a folder.

 

All of the endpoints we'll cover in this blog post have been provided in a shared Postman collection. To import the collection into Postman click the "Run in Postman" button below.

 

button.svg

 

The requests in the collection are configured to use the test user we created in the v1 REST API - Part 1 - Introduction post.

 

To retrieve the children for a folder we need it's id, to help get started we have provided three aliases, -root, -my- and -shared-. As the name suggests -root- maps to the root folder of the repository, -my- maps to the currently authenticated users home on-premise and to the users private site on my.alfresco.com and finally -shared- maps to the repository's shared folder.

 

Let's retrieve the children of the repositories root folder, to do this we use http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children. If you've executed this request in Postman you should see something similar to the screenshot below (you can of course alternatively use curl or your favourite REST client).

 

Screen Shot 2016-10-13 at 21.55.21.png

 

If you've used the v1 REST API before you should recognise the collection response format. Each child node is represented within an entry object which is contained in an entries array. New v1 REST APIs are following a "performance first" principle, meaning by default, we only return the data that is the most performant to collect. More data can be requested but the client has to make a conscious decision to request more data and sacrifice a slight performance hit.

 

Let's build on the first request and ask for properties and a list of aspect names to be included for each node, we do that by using the new include query parameter as follows:

 

http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?include=properties,aspectNames

 

One of the entry objects including the extra information is shown in the response snippet below.

 

"entry": {
"aspectNames": [
"cm:titled",
"cm:auditable",
"app:uifacets"
],
"createdAt": "2016-10-05T19:44:11.127+0000",
"isFolder": true,
"isFile": false,
"createdByUser": {
"id": "System",
"displayName": "System"
},
"modifiedAt": "2016-10-05T19:44:56.559+0000",
"modifiedByUser": {
"id": "System",
"displayName": "System"
},
"name": "Data Dictionary",
"id": "0be671ab-5fd9-4cc2-b93f-10fa0939e70f",
"nodeType": "cm:folder",
"properties": {
"cm:title": "Data Dictionary",
"cm:description": "User managed definitions",
"app:icon": "space-icon-default"
},
"parentId": "03acc816-b42f-4d87-ab1f-4d4ae16e73ef"
}

 

The 2nd request in the Postman collection can also be used to see this in action. The list of extra data that can be included is documented for each endpoint in the OpenAPI Specification, see http://localhost:8080/api-explorer/#!/nodes/getNodeChildren for an example. We'll cover customising the responses in much more detail in a future blog post.

 

As you'll see from the screenshot above the pagination object tells us a maximum of 100 items is returned by default, this can be controlled with the maxItems query parameter and the starting point can be controlled via the skipCount query parameter, for example, to get items 3 through 5 use the following URL (3rd request in the Postman collection):

 

http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?skipCount=2&maxItems=3

 

The pagination object in the response will indicate there are more results to retrieve via the hasMoreItems property. If it's possible to determine the total number of items available the totalItems property will also be present as shown below.

 

"pagination": {
"count": 3,
"hasMoreItems": true,
"totalItems": 7,
"skipCount": 2,
"maxItems": 3
}

 

The items returned can also be filtered via the where query parameter, the filtering supported is specific to each endpoint, again this is documented in the OpenAPI Specification, see [http://localhost:8080/api-explorer/#!/nodes/getNodeChildren] for an example, we'll take a look at a couple of them here.

 

Firstly, the isFile property can be used to just return nodes that represent content i.e. a subtype of cm:content by using the following URL (4th request in the Postman collection):

 

http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?where=(isFile=true)

 

The same result can be achieved by using the isFolder property too, for example http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?where=(isFolder=false)

 

As there are no files in the root of the repository we get an empty response, we'll cover creating files in the next post.

 

To filter the results by a specific content type nodeType can be used in the where clause, for example, to retrieve just the Sites folder use the following URL (5th request in the Postman collection):

 

http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?where=(nodeType=st:sites)

 

This will result in a single result as shown below.

 

{
"list": {
"pagination": {
"count": 1,
"hasMoreItems": false,
"totalItems": 1,
"skipCount": 0,
"maxItems": 100
},
"entries": [
{
"entry": {
"createdAt": "2016-10-05T19:44:14.968+0000",
"isFolder": true,
"isFile": false,
"createdByUser": {
"id": "System",
"displayName": "System"
},
"modifiedAt": "2016-10-05T19:44:29.073+0000",
"modifiedByUser": {
"id": "System",
"displayName": "System"
},
"name": "Sites",
"id": "4f00d061-cdad-42b5-bfd6-8b0759a7fa7c",
"nodeType": "st:sites",
"parentId": "03acc816-b42f-4d87-ab1f-4d4ae16e73ef"
}
}
]
}
}

 

To retrieve all nodes of a specific type and it's sub-types use the INCLUDESUBTYPES moniker, for example where=(nodeType='my:specialNodeType INCLUDESUBTYPES')

 

Finally, the items returned can also be ordered via the orderBy query parameter, the ordering supported is specific to each endpoint, once again this is documented in the OpenAPI Specification, see http://localhost:8080/api-explorer/#!/nodes/getNodeChildren for an example.

 

By default, the nodes/{id}/children endpoint uses orderBy=isFolder DESC,name ASC as the default sort order i.e. folders first alphabetically followed by files. To mix files and folders and order them alphabetically in reverse order use the following URL (6th request in the Postman collection):

 

http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/-root-/children?orderBy=name DESC

 

So far, we've only used -root- as the folder id, we can of course also use an explicit id, for example to retrieve the children of my Data Dictionary folder I would use the following URL:

 

http://localhost:8080/alfresco/api/-default-/public/alfresco/versions/1/nodes/0be671ab-5fd9-4cc2-b93f-10fa0939e70f/children

 

You've now learnt how to navigate the repository, in the v1 REST API - Part 3 - Creating Nodes we'll see how to create files and folders.

Alfresco has had a REST API for a long time but it organically grew over time as new services and capabilities were added. There were no guidelines defined which culminated in an inconsistent hard to use API.

 

This journey was covered in a Tech Talk Live earlier this year and in a presentation at BeeCon but there hasn't been much coverage of how to use the API, this is the first part of a series of blog posts that aim to introduce you to and provide hands-on experience of using the v1 REST API.

 

We believe a key requirement for a modern day REST API is an OpenAPI Specification (formerly known as Swagger). To provide as much value as possible we decided to manually generate the specification based on the endpoints available in 5.1. Given these hadn't changed since 4.2, customers would have a full specification of the v1 REST API independent of the Alfresco One product. Furthermore, this provides us with a strong API contract that we can use against the next release to ensure no regressions have crept into the product.

 

One benefit you gain from having an OpenAPI Specification for your API is the tooling support, most notably the Swagger UI. We used this to provide customers with the API Explorer, an environment to allow you to experiment with APIs without having to install anything. A majority of the manually written reference documentation on docs.alfresco.com has also been replaced by the API Explorer.

 

api-explorer.png

At the moment the underlying repository is 5.1 so the live API Explorer does not show any of the new APIs added in the recent 5.2.b Early Access Community Release, providing access to all versions is something we're currently discussing. We are also contemplating how to release an API Explorer at the end of every sprint so you can keep track of progress in between releases.

 

In order to follow along with the forthcoming blog posts you'll need an environment to do so, firstly download and install the 5.2.b Early Access Community Release. We'll also be using the Postman client to execute the requests and providing them as a collection. Finally, to setup the API Explorer locally, download the WAR, rename it to api-explorer.war and copy it to $ALF_HOME/tomcat/webapps.

 

Login to Share, go to the Manage Users page and create a user named "test" with a password "test".

 

In the next post we'll get started by looking at navigating the repository, in the meantime, start your server and have a look through the REST API documentation at http://localhost:8080/api-explorer.

Using vue.js with Aikau

Posted by ddraper Oct 7, 2016

Introduction

In the recent State of JavaScript 2016 survey results I noticed that vue.js was gaining massively in popularity as a front-end framework so I thought it might be an interesting experiment to see how easily it could be leveraged within Aikau. I'll admit that I haven't delved to deeply into vue.js in any meaningful way beyond reading the first few pages of the guide so don't expect this to provide any earth-shattering insight into what you can and can't do with it, and I have no real opinion on it yet beyond having noticed it's growing adoption. This post can be considered a more general guide to how you can go about using the JS framework of your choice within Aikau.  In this example I'm going to create an extension that provides a Share dashlet rendering a very simple vue.js component (the obligatory "Hello World" component in fact).

 

Hello Vue.js

The first thing I noticed when going through the guide was how simple it was - write a template and create a component that targets that template. This approach lends itself very nicely to Aikau which breaks widgets down into JS, CSS, templates and localization files. The template for the example looks like this:

 

<div id="app">
  {{ message }}
</div>

 

...and the JavaScript looks like this:

 

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

 

The idea is that the Vue instance uses the el CSS style selector to target the fragment of DOM to convert into a component and substitutes the {{ message }} token with the value of message.

 

Adding a Package Declaration

The other great thing I found out was that vue.js is written to support AMD. This means that we can just declare it as a new package for Aikau to consume. I downloaded the source files and placed them in the META-INF/js/lib folder of my extension JAR and defined a couple of new packages ("vue" for the vue.js libary, and "blog" for my custom components) like this:

 

<extension>
  <modules>
    <module>
      <id>vue.js extension</id>
      <auto-deploy>true</auto-deploy>
      <evaluator type="default.extensibility.evaluator"/>
      <configurations>
        <config evaluator="string-compare" condition="WebFramework" replace="false">
          <web-framework>
            <dojo-pages> 
                <packages> 
                    <package name="vue" location="js/lib" main="vue"/> 
                    <package name="blog" location="js/blog"/> 
                </packages>
            </dojo-pages>
          </web-framework>
        </config>
      </configurations>
    </module>
  </modules>
</extension>

 

Creating a Widget

The next step was to create a widget that made use of the vue.js package. This widget would comprise of the JavaScript file and an HTML template file. The JavaScript file looks like this:

 

define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/VueExample.html",
        "alfresco/core/Core",
        "vue"], 
        function(declare, _WidgetBase, _TemplatedMixin, template, AlfCore, Vue) {
   
   return declare([_WidgetBase, _TemplatedMixin, AlfCore], {

      templateString: template,

      postCreate: function vue_VueExample__postCreate() {
         new Vue({
            el: "#" + this.id,
            data: {
               message: "Hello Vue.js!"
            }
         });
      }
   });
});

 

The key things to notice here are that we're importing the "vue" package, and the "VueExample.html" template. You might also notice that the call to create a new Vue component uses a slightly different el attribute - in this case we're just using the id attribute that will be generated for us by Aikau (unless we explicitly provide one)... this is guaranteed to be unique to ensure that vue.js will only work with the right widget instance!

 

The template file is also very similar:

 

<div class="blog_VueExample">
  <p>{{ message }}</p>
</div>

 

...the only difference is that we're specifying a BEM style class for our widget. We use BEM throughout our Aikau widgets to ensure that styles do not "bleed" between widgets inadvertently.

 

Defining the Dashlet

I described in detail how to create an Aikau dashlet in this previous blog post so I won't dwell on it too much - this is the JavaScript controller for defining it:

 

model.jsonModel = {
   rootNodeId: args.htmlid,
   pubSubScope: instance.object.id,
   services: [],
   widgets: [
      {
         name: "alfresco/dashlets/Dashlet",
         config: {
            title: "vue.js example",
            bodyHeight: args.height || null,
            componentId: instance.object.id,
            widgetsForToolbar: [],
            widgetsForBody: [
               {
                  name: "blog/VueExample"
               }
            ]
         }
      }
   ]
};

 

The main thing to note is that the widgetsForBody is where we're placing our new vue.js Aikau widget.

 

The End Result

After building the extension JAR and dropping it into the share/WEB-INF/lib folder and restarting Share the dashlet is available to be added to dashboards and looks like this:

vuejs dashlet.png

 

Summary

OK.... so it's not the most exciting showcase of the capabilities of vue.js, but if you're interested in using vue.js in your Share customizations then using this approach is probably a good place to start. I'll read a bit more into vue.js and see if it's something that we can use more of in Aikau as it does seem like a good fit for providing enhanced templating and binding capabilities. I'd be interested to know people's thoughts on this - please let me know in the comments section if you've tried out vue.js and what you think of it.

 

You can find the source code for this example in this GitHub repository.

Introduction

In my last blog post I showed how easy it is to add new items into the Aikau Document Library "Create Content..." menu. In this particularly example I was showing how to create markdown content but in order to do this I actually needed to create a new widget. It's been suggested in the past that I should try to provide more examples of how to create custom widgets so I figured that it would be as good a time as any...

 

Aikau already had widgets for both editing content with syntax highlighting and previewing markdown but these didn't yet exist as a single entity. The great thing about Aikau is that it incredibly easy to combine existing widgets together to make a new widget that can be referenced as an individual entity. We learned our lessons from the original YUI2 widgets that it is important to define fine-grained widgets that can be easily composed rather than creating large-grained coarse widgets that become increasingly hard to customize over time. The Dojo _WidgetBase module that Aikau builds upon also provides excellent inheritance capabilities that we are able to leverage.

 

So our objective was to create a new widget that allowed editing of markdown and showed a preview of the changes as they happened. As this widget is to be used for creating content submitted through a form then it needed to also inherit from BaseFormControl.  This meant that obvious candidate to extend would be the existing CodeMirrorEditor form control as it had the correct ancestry and could be configured to show markdown syntax highlighting.

 

Creating the Widget

The initial code to extend the widget looks like this:

 

define(["dojo/_base/declare",
        "alfresco/forms/controls/CodeMirrorEditor"],
        function(declare, CodeMirrorEditor) {
   return declare([CodeMirrorEditor], {
   });
});

 

This essentially defines a new widget that is exactly the same as the CodeMirrorEditor, now we can start to change a few things. First we know that we want to use markdown highlighting - this is done by setting the editMode:

 

define(["dojo/_base/declare",
        "alfresco/forms/controls/CodeMirrorEditor"],
        function(declare, CodeMirrorEditor) {
   return declare([CodeMirrorEditor], {
      editMode: "markdown"
   });
});

 

We also know that we want to change how the form control itself is constructed. We do this by extending (through a call to this.inherited(arguments) the createFormControl function:

 

define(["dojo/_base/declare",
        "alfresco/forms/controls/CodeMirrorEditor"],
        function(declare, CodeMirrorEditor) {
   return declare([CodeMirrorEditor], {
      editMode: "markdown",

      createFormControl: function alfresco_forms_controls_MarkdownWithPreviewEditor__createFormControl(config, domNode) {
         var widget = this.inherited(arguments);
         return widget;
      }
   });
});

 

We want to also create the Markdown widget. This requires a new DOM element created, and a new Markdown widget created that binds to it:

 

createFormControl: function alfresco_forms_controls_MarkdownWithPreviewEditor__createFormControl(config, domNode) {
   var widget = this.inherited(arguments);
   this.previewNode = domConstruct.create("div", { 
      style: "height:" + this.height + "px;width:" + this.width + "px;",
      className: "alfresco-forms-controls-MarkdownWithPreviewEditor__previewNode"
   }, this._controlNode);
   var preview = this.createWidget({
      name: "alfresco/html/Markdown",
      config: {
         markdown: this.initialValue
      }
   });
   preview.placeAt(this.previewNode);
   return widget;
},

 

In order for the Markdown widget to show the changes as they happen it is necessary to define a subscription topic that it can uses to listen for changes. By calling the generateUuid function we can create a unique topic to prevent any "cross talk" between widgets.

 

this.generatePreviewTopic = this.generateUuid();

this.previewNode = domConstruct.create("div", { 
   style: "height:" + this.height + "px;width:" + this.width + "px;",
   className: "alfresco-forms-controls-MarkdownWithPreviewEditor__previewNode"
}, this._controlNode);

var preview = this.createWidget({
   name: "alfresco/html/Markdown",
   config: {
      markdown: this.initialValue,
      subscriptionTopics: [this.generatePreviewTopic]
   }
});
preview.placeAt(this.previewNode);

 

Now we need to ensure that when the user makes edits they are published on the generatePreviewTopic that the Markdown widget will be subscribed to. This means extending the onEditorChange function to publish the new value each time it is updated.

 

onEditorChange: function alfresco_forms_controls_MarkdownWithPreviewEditor__onEditorChange(editor, changeObject) {
   this.inherited(arguments);
   this.alfPublish(this.generatePreviewTopic, {
      markdown: this.lastValue
   });
}

 

Now we have an extended version of the CodeMirrorEditor that is configured for markdown editing, with an additional Markdown preview widget that is updated on changes.

 

Summary

Although this doesn't show every change (these can be reviewed in the associate pull request) I've tried to capture the essential elements of how two separate widgets were pulled together to make a new one. The steps I took to create this were obvious to me because of my familiarity with Aikau - but if you need help on getting started creating a new widget then just create a question in the discussion forums and I'll be happy to provide you with suggestions on how to get started.

I recently saw a really interesting blog post from Parashift on creating custom editors for Share. It was interesting because they were showing how to use the showdown.js markdown library as a previewer and that is exactly the same markdown library that Aikau uses for its Markdown widget.

 

I thought it might be an interesting exercise to compare the effort required to create the same editor in Aikau - this isn't to take anything away from what Parashift have shown which is a perfectly valid customization for Share - I just wanted to see what it would be like to do it using Aikau.

 

Full disclose: I had to create a new widget and make some modifications to Aikau to support this blog post, but these changes are all now available in the 1.0.89 release of Aikau

 

I'm going to show this as a customization of the Aikau Document Library (rather than the YUI2 version) and this means that the editor will be displayed inline rather than on a separate page. I've discussed the Aikau Document Library in previous blog posts here and here so I would recommend reading through those to understand how you can customize Share to make use of the Aikau Document Library.

 

The main thing we want to do is to define a new AlfCreateContentMenuItem that can be used to show a dialog for creating markdown content. The doclib.lib.js import file that you'll use to build the model for the Document Library contains the useful helper function generateCreateContentMenuItem. We can call this with configuration for the menu item like this:

 

var createMarkdownMenuItem = generateCreateContentMenuItem({
   menuItemLabel: "Create Markdown", 
   dialogTitle: "Create Markdown", 
   iconClass: "alf-textdoc-icon", 
   modelType: "cm:content", 
   mimeType: "text/markown",
   dialogWidth: "900px",
   contentWidgetName: "alfresco/forms/controls/MarkdownWithPreviewEditor", 
   contentWidgetConfig: { 
      width: 300, 
      height: 250
   }
});

 

The main thing to note here is that we're setting the contentWidgetName to the new MarkdownWithPreviewEditor widget and setting a mimeType to "text/markdown".

 

We now need to ensure that the menu item is included in the Document Library model, this can be easily achieved in the configuration passed to the getDocLib function that is called to build the main model:

 

var docLib = getDocLib({
   siteId: page.url.templateArgs.site, 
   containerId: "documentLibrary",
   additionalCreateContentItems: [createMarkdownMenuItem]
});

 

...and that's it !!

 

When you access the page you'll see the following:

 

The full source of the JavaScript controller for the WebScript is here (the descriptor, template and properties files are the same as in the other blogs posts)...

 

<import resource="classpath:/alfresco/site-webscripts/org/alfresco/share/imports/share-header.lib.js">
<import resource="classpath:/alfresco/site-webscripts/org/alfresco/share/imports/share-footer.lib.js">
<import resource="classpath:alfresco/site-webscripts/org/alfresco/aikau/{aikauVersion}/libs/doclib/doclib.lib.js">

// Get the services and widgets for the header...
var services = getHeaderServices();
var widgets = getHeaderModel(msg.get("aikau.doclib.title"));

// Build a list of services for both the header AND the Document Library...
services = services.concat(getDocumentLibraryServices());

// Create the markdown creation menu item...
var createMarkdownMenuItem = generateCreateContentMenuItem({
   menuItemLabel: "Create Markdown", 
   dialogTitle: "Create Markdown", 
   iconClass: "alf-textdoc-icon", 
   modelType: "cm:content", 
   mimeType: "text/markown",
   dialogWidth: "900px",
   contentWidgetName: "alfresco/forms/controls/MarkdownWithPreviewEditor", 
   contentWidgetConfig: { 
      width: 300, 
      height: 250
   }
});

// Create the Document Library model...
var docLib = getDocLib({
   siteId: page.url.templateArgs.site, 
   containerId: "documentLibrary",
   additionalCreateContentItems: [createMarkdownMenuItem]
});

// Add the Document Library model to the header widgets...
widgets.push(docLib);

// The footer wraps everything...
model.jsonModel = getFooterModel(services, widgets);

// Always include user membership data for any group membership based evaluations...
model.jsonModel.groupMemberships = user.properties["alfUserGroups"];

I super excited to welcome Francesco Corti to the Alfresco team as our new Product Evangelist!

 

Many of you probably know Francesco from his participation in the Alfresco Community, Order of The Bee and his excellent work on AAAR.

 

Francesco is no stranger to Alfresco, he started working with Alfresco 3.4 both on Enterprise and Community edition. Has almost 20 years of experience in developing software, designing architecture and building solutions in ECM, BPM and Business Intelligence.

 

In December last year I picked up the role of Product Manager for Developer Platform, but I haven’t been able to fully transition into this role since we needed a new evangelist to take over my old role. I’m excited to finally have Francesco on the team, and I look very much forward to working with him.

 

Francesco joins us at an exciting time, as we have a lot of upcoming projects such as SDK 3.0 and Application Development Framework. Francesco will drive the evangelism strategy to ensure we maximize the impact and adoption.

 

Francesco and Kristen will work together on growing our community, host events (Tech Talk Live, Office Hours etc), create content and nourish our existing community.

 

Welcome to Alfresco, Francesco!

Filter Blog

By date: By tag: