API

csApiSession

Use the csApiSession module to access methods to get assets, execute queries or command handlers.

  • As the name says, it uses the current user session so you don’t need to care about that
  • You do not need to add csApiSession to your module dependencies, it’s provided by framework by default
  • To use it, simply inject csApiSession in the function you need it

Please note that csApiSession is deprecated and replaced with ApiService in Angular 2+. When csApiSession is used, it works by calling the ApiService under the hood.

csApiSession.execute(commandId, method, data)

It executes the method of a command with provided data.
Parameters:

  • commandId {String} - Command id
  • data {Object}
  • method {String} - Method name

It returns:

  • {Promise}

csApiSession.execute("csYoutubeVideoDialog.CreateYoutubeVideoAsset", "execute", videoData).then(result => {
    return result.assetId;
});

csApiSession.execute(id, data)

It executes a command with provided data.

Parameters:

  • commandId {String} - Command id
  • data {Object}

It returns:

  • {Promise}

csApiSession.execute("com.censhare.api.dam.dialog.create.asset.new", {
    assetType: data.assetType,
    assetData: data.assetData,
    accessRights: accessRights
}).then(...);

csApiSession.asset.get()

It gets an asset by id or multiple assets if an array of ids is given.

Parameters:

  • id {number|Array} - Asset ID(s)
  • options - possible options are:
    • version (when getting single asset)
    • currversion (when getting single asset)
    • ccn (when getting single asset)
    • view: {modelFormat: ‘update’|‘logical’, traits:‘trait1,trait2,…’, trafo: view-model-transformation}

It returns:

  • {Promise}

params = {
    view: {
        trafo: "censhare:cs5.query.asset.list",
        viewName: csViewNameResolver.getViewFromRowHeight("2")
    }
};
assetIt getsPromise = csApiSession.asset.get(assetIds, params).then(function (assets) {
    angular.forEach(assets.asset, function (asset) {
        $scope.assetsMap[asset.traits.ids.id] = asset;
    });
 
}, function (error) {
    csNotify.error("csCommonTranslations.access_rights", error);
});

csApiSession.asset.query()

Query for assets.

Parameters:

  • query - Query object

It returns:

  • {Promise}

var query = {};
query.condition = [
    {name: 'censhare:asset.type', op: 'LIKE', value: 'person.*'},
    {name: 'censhare:address.uri-mailto', op: 'LIKE', value: token + '*'}
];
query.limit = 20;
 
csApiSession.asset.query(query).then(function (result) {
    ...
});

csApiSession.asset.liveQuery()

Query for assets with continuous updates, it sends updates as promise notify and returns no values in promise resolve.

Parameters:

  • query - Query object
  • destructible - Object which provides ‘onDestroy’ callback. Live query is cancelled when destructible is destroyed.

It returns:

  • {Promise}

var queryBuilder = new csQueryBuilder(),
    query;
 
queryBuilder.condition("censhare:asset.type", "category.topic.*");
queryBuilder.condition("censhare:asset-flag", "ISNULL", null);
queryBuilder.order().orderBy("censhare:asset.name", true);
query = queryBuilder.build();
 
csApiSession.asset.liveQuery(query, $scope).then(null /*onResolve*/, null /*onError*/, function onNotify(result) {
    ...
}) ;

csApiSession.masterdata.lookup()

Lookup on master data values. No matter how many records are returned by the command, the results are always provided as an array.

Parameters:

  • lookup - Object which specify conditions

It returns:

  • {Promise}

csApiSession.masterdata.lookup({
    lookup : [{
        table : "feature",
        condition : [{'name' : 'value_type', 'value' : 10}]
    }]
}).then(function (result) {
    angular.forEach(result.records.record, function (record) {
        ...
    });
});
 
csApiSession.masterdata.lookup({
    lookup: [
        {
            table: "partyrel",
            condition: [
                {name: 'enabled', value: 1},
                {name: 'child_id', value: csApiSession.session.getUserId()}
            ]
        }
    ]
}).then(function (result) {
    if (result.records && result.records.record) {
        ...
    }
});

csApiSession.masterdata.singleValueLookup()

Lookup on master data values. If the result contains only one record, the return record array is omitted and the single result is directly provided as an object.
Parameters:

  • lookup - Object which specify conditions

It returns:

  • {Promise}

this.csApiSession.masterdata.singleValueLookup({
    lookup : [{
        table : "asset_rel_typedef",
        condition : [{
            name : "key",
            value : that.relationType
        }]
    }]
}).then(function (result) {
    if (result.records && result.records.record) {
        ...
    }
});

csApiSession.permission.checkAssetIndependentPermission()

Parameters:

  • permissionKey String that identifies the permission

It returns:

  • {Promise}

csApiSession.permission.checkAssetIndependentPermission("asset_export_all").then(function (value) {
    scope.export_permission = value && !!value.permission;
});
csApiSession.permission.checkAssetIndependentPermission("asset_checkin_new").then(function (value) {
    scope.create_asset_permmission = value && !!value.permission;
});

csApiSession.permission.checkAssetPermission()

Parameters:

  • permissionKey String that identifies the permission
  • assetRef - Asset reference

It returns:

  • {Promise}

csApiSession.permission.checkAssetPermission("asset_version_restore", self_versioned).then(function (value) {
    scope.restoreVersionAllowed = value && !!value.permission;
});

csApiSession.relation.add(assetId, relatedAssetId, relationType, direction)

It creates a new relation with a specified direction. Please note that you either are allowed to pass a set of ids for parentId or for childId. It is forbidden to pass an array for both parameters.

Parameters:

  • parentIds {Number|Array }
  • childIds {Number|Array }
  • reltype {String}
  • direction {String} relation direction

It returns:

  • @returns {Promise}

csApiSession.relation.add(addIds, assetId, relation.key, relation.direction);

csApiSession.relation.create(parentIds, childIds, reltype, isFeatureRef)

It creates a new relation. Please note that you either are allowed to pass a set of ids for parentId or for childId. It is forbidden to pass an array for both parameters.

Parameters:

  • parentIds {Number|Array }
  • childIds {Number|Array }
  • reltype {String}
  • isFeatureRef {Boolean}

It returns:

  • @returns {Promise}

csApiSession.relation.create(csAssetUtil.getAssetIdFromAssetRef(parentAssetRef),
    csAssetUtil.getAssetIdFromAssetRef(assetRef), 'user.task.', false).then(function () {
    ...
});

csApiSession.relation.delete(parentId, childId, reltype, isFeatureRef)

It removes and existing relation.

Parameters:

  • parentIds {Number|Array }
  • childIds {Number|Array }
  • reltype {String}
  • isFeatureRef {Boolean}

It returns:

  • @returns {Promise}

csApiSession.relation.delete(context.traits.ids.id, rel.oppositeAssetId, rel.key, true);

csApiSession.resourceassets.lookup(filter)

Queries resource assets filtered by a given filter condition.

Parameters:

  • filter {Object} - Filter object. The following filter properties can be set:
    • sourceAssetRef : Key of the source asset, on which the resource asset (e.g. transformation) will be applied (optional). Only resource assets, whose source filter matches the given asset, will be included into the result.
    • targetAssetRef : Key of the target asset, on which the resource asset (e.g. transformation) will be applied (optional). Only resource assets, whose target filter matches the given asset, will be included into the result.
    • usage: Usage key of the resource asset (optional). Examples: “censhare:indesign-content-transformation”, “censhare:chart-data-transformation”, “censhare:content-transformation”
    • assetType: Exact type of the resource asset (optional). Sub types are not included.
    • mimetype: Mimetype of the transformation result (only for transformation resource assets) (optional).
    • format: Format key of the transformation result (only for transformation resource assets) (optional).

It returns:

  • {Promise} The promise is fulfilled with a set of resource assets matching the filter condition. The result structure looks like this:

{
    resourceAssets : [
        {
            assetRef        : Asset ID (key) of the resource asset
            resourceKey     : Unique resource asset key
            name            : Localized name of the resource asset
            assetType       : Type of the resource asset
            domain1         : First domain
            domain2         : Second domain
            selectionNeeded : Flag if this resource asset requires a (asset) selection.
            usages          : [ Array of usage keys ]
            formats         : [ Array of output formats and mimetypes (of transformations)
                { format : "xml", mimetype : "text/xml"},
            ... ],
            urlTemplate     : Template URL (only set for transformations). Example: censhare:///service/assets/asset/id/${asset-id}/transform;key=${resource-key}
        },
        ...
    ]
}

csApiSession.resourceassets.lookup({
    sourceAssetRef: assetRef,
    usage: "censhare:chart-data-transformation"
}).then(function (result) {
    result.resourceAssets.forEach(function (resourceAsset) {
        transformations.push({
            key: resourceAsset.resourceKey,
            name: resourceAsset.name
        });
    });
});

csApiSession.session.getLocale()

It returns the locale for the current API session.

csApiSession.session.getTimezone()

It returns the time zone for the current API session.

csApiSession.session.getUserAssetReader()

It returns a read for the user asset data.
It returns:

  • csReader

var userAsset = csApiSession.session.getUserAssetReader().getValue();
 
csApiSession.session.getUserAssetReader().registerChangeListenerAndFire(userAsset => {
    if (userAsset && userAsset.thumbnail && userAsset.thumbnail.url && userAsset.thumbnail.url.indexOf("/icon/") === -1) {
        userMenu
            .setIcon(userAsset.thumbnail.url)
            .setTitle(userAsset.traits.display.name)
            //.addCssClass('cs-has-profile-image')
        ;
    } else {
        userMenu
            .setIcon("cs-icon-user-alt")
            .setTitle(userAsset ? userAsset.traits.display.name : 'csCommonTranslations.user');
    }
}, $scope);

csApiSession.session.getUserId()

It returns the user party table id.

csApiSession.session.resolveUrl(url)

It returns a qualified HTTP URL for either a censhare URL (censhare://…) or a path relative to the base of the application (/img/no-image.jpeg)

Parameters:

  • url A REST or censhare url

It returns:

  • url that could be used

uploadIcon = csApiSession.session.resolveUrl('rest/service/resources/icon/assettype-other./minsize/128/background/dark/file');
previewUrl = csApiSession.session.resolveUrl('/img/no-image.jpeg');

csApiSession.workspace.getUserPreferences()

It returns:

  • {Promise} Current user preferences

csApiSession.workspace.getUserPreferences().then(function (data) {
    ...
});

csQueryBuilder

It provides an API to generate a query object for csApiSession.asset.query() and csApiSession.asset.liveQuery() methods.

  • Include csQueryBuilder to module dependencies

csQueryBuilder has it’s own methods, but also supports csQueryBuilder.Node interface, so has all methods provided by csQueryBuilder.Node.

var query,
    currentOffset,
    liveQueryPromise;
 
query = new csQueryBuilder();
 
//to return assets from with indexes 100-109
query.setLimit(10); //set limit to 10
query.setOffset(100); //set query offset to 100
 
//set view parameters
query.view().setTransformation("censhare:cs5.query.asset.list");
query.view().setViewName("assetQueryList");
 
//to set conditions
query.condition("censhare:asset.id", assetId);
query.condition('censhare:asset.type', 'picture.');
query.condition('censhare:asset-flag', 'is-template');
query.condition('censhare:asset.checked_out_by', "=", userId);
 
//build a query
liveQueryPromise = csApiSession.asset.liveQuery(query.build());

build()

It generates a query object.
It returns:

  • {QueryObject}

setLimit(limit)

It sets query limit. Limit specifies maximal amount of assets to be returned by query.
Parameters:

  • limit {Number}

setOffset(offset)

Offset of a query is used to support pagination. Using query limit and offset, amount and range of returned asset could be specified.
Parameters:

  • offset {Number}

setQueryParamFilterValue(queryParamFilter)

It sets custom value for csQueryParamFilter. csQueryParamFilter is used to filter asset behaviours. The Behavior with queryParamFilter will be included into assets which was queries with a query with the same queryParamFilter.
Parameters:

  • queryParamFilter {String}

order()

It creates or returns existing one csQueryBuilder.Order object. Is used to specify query order and grouping.
It returns:

  • {csQueryBuilder.Order}

view()

It creates or returns existing one View object. Is used to specify query view: view transformation, traits which will be included, view name and etc.
It returns:

  • {csQueryBuilder.View}

csQueryBuilder.Node

Node object is used to set conditions for a query.

condition(name, value)

It creates condition to check if feature is equal to provided value. Can be used only for features which can have Null value. If a few conditions are added to the query, AND logic is used to combine them.
Parameters:

  • name {String} - Name of a feature
  • value {String} - Value of the feature

condition(name, op, value)

It creates condition to check feature value using operator provided. Can be used only for features which can have Null value. If a few conditions are added to the query, AND logic is used to combine them.
Parameters:

  • name {String} - Name of a feature
  • value {String} - Value of the feature
  • op {String} - Operator. Possible values for operator: “=”, “<”, “<=”, “>”, “>=”, “!=”, “like”, “IN”, “ISNULL”, “NOTNULL”

condition(name)

It creates condition to check if feature is exist. Can be used only for features which can have Null value. If a few conditions are added to the query, AND logic is used to combine them.
Parameters:

  • name {String} - Name of a feature

and()

Adds Query Node to existing Node with AND logic, all subnodes and condition of which are combined with AND logic: node AND (cond1 AND cond2).
It returns:

  • {csQueryBuilder.Node}

not()

Adds Query Node to existing Node with AND NOT logic: node AND NOT(cond1 AND cond2)
It returns:

  • {csQueryBuilder.Node}

or()

Adds Query Node to existing Node with AND logic, all subnodes and condition of which are combined with OR logic: node AND (cond1 OR cond2)
It returns:

  • {csQueryBuilder.Node}

relation(direction, type)

Adds relation conditions to existing Node, which is used to search by related assets. Relation condition and subcondition are added using AND logic to existing node.
Parameters:

  • direction {String} - relation direction
  • type {String} - relation type

//all children for asset with id = assetId with relation type 'user.'
queryBuilder.relation('parent', 'user.').condition('censhare:asset.id', assetId);

It returns:

  • {csQueryBuilder.Node}

csQueryBuilder.Order

Allows to set order for result.

query.order().orderBy("censhare:asset.name", false);

orderBy(property, isAssending)

It sets an order for result assets

csQueryBuilder.View

Allows to set custom representation for assets.

var query = new csQueryBuilder(),
    view;
view = query.view();
view.setTransformation("censhare:cs5.query.asset.list");
view.setViewName(csViewNameResolver.getViewFromRowHeight("2"));

setTransformation(assetKey)

It sets a XSLT transformation asset for a query result, which will transform standard asset representation to a new one.
“censhare:cs5.query.asset.list” transformation is used in censhare to add required information for standard item renderers.

setViewName(viewName)

It sets workspace view name, which allows to specify additional data, which should be added into asset structure.
Use csViewNameResolver to get appropriate viewName for asset representation.

setModelFormat(model)

It sets format of asset View Model. If no format is set, logical model of the asset will be returned. Set “update” as a model format to get asset Update model.

csContextProvider

getAssetId()

It returns the asset id of the context.

getApplicationNames(): string[]

It returns the names of the applications supported by this context.

getBasicAssetApplication()

It returns the com.censhare.api.applications.asset.BasicAssetApplication instance

getLiveApplicationInstance(name)

It returns a reader that contains the requested application instance.

hasApplication(name)

It returns if the given application is supported by the context. This is faster than searching in the array provided by getApplicationNames, because a simple has based lookup is performed.

hasBasicAssetApplication()

It returns if the com.censhare.api.applications.asset.BasicAssetApplication is supported by the context.

csQueryBuilder

It provides an API to generate a query object for csApiSession.asset.query() and csApiSession.asset.liveQuery() methods.

  • Include csQueryBuilder to module dependencies

csQueryBuilder has it’s own methods, but also supports csQueryBuilder.Node interface, so has all methods provided by csQueryBuilder.Node.

var query,
    currentOffset,
    liveQueryPromise;
 
query = new csQueryBuilder();
 
//to return assets from with indexes 100-109
query.setLimit(10); //set limit to 10
query.setOffset(100); //set query offset to 100
 
//set view parameters
query.view().setTransformation("censhare:cs5.query.asset.list");
query.view().setViewName("assetQueryList");
 
//to set conditions
query.condition("censhare:asset.id", assetId);
query.condition('censhare:asset.type', 'picture.');
query.condition('censhare:asset-flag', 'is-template');
query.condition('censhare:asset.checked_out_by', "=", userId);
 
//build a query
liveQueryPromise = csApiSession.asset.liveQuery(query.build());

build()

It generates a query object.
It returns:

  • {QueryObject}

setLimit(limit)

It sets query limit. Limit specifies maximal amount of assets to be returned by query.
Parameters:

  • limit {Number}

setOffset(offset)

Offset of a query is used to support pagination. Using query limit and offset, amount and range of returned asset could be specified.
Parameters:

  • offset {Number}

setQueryParamFilterValue(queryParamFilter)

It sets custom value for csQueryParamFilter. csQueryParamFilter is used to filter asset behaviours. The Behavior with queryParamFilter will be included into assets which was queries with a query with the same queryParamFilter.
Parameters:

  • queryParamFilter {String}

order()

It creates or returns existing one csQueryBuilder.Order object. Is used to specify query order and grouping.
It returns:

  • {csQueryBuilder.Order}

view()

It creates or returns existing one View object. Is used to specify query view: view transformation, traits which will be included, view name and etc.
It returns:

  • {csQueryBuilder.View}

csQueryBuilder.Node

Node object is used to set conditions for a query.

condition(name, value)

It creates condition to check if feature is equal to provided value. Can be used only for features which can have Null value. If a few conditions are added to the query, AND logic is used to combine them.
Parameters:

  • name {String} - Name of a feature
  • value {String} - Value of the feature

condition(name, op, value)

It creates condition to check feature value using operator provided. Can be used only for features which can have Null value. If a few conditions are added to the query, AND logic is used to combine them.
Parameters:

  • name {String} - Name of a feature
  • value {String} - Value of the feature
  • op {String} - Operator. Possible values for operator: “=”, “<”, “<=”, “>”, “>=”, “!=”, “like”, “IN”, “ISNULL”, “NOTNULL”

condition(name)

It creates condition to check if feature is exist. Can be used only for features which can have Null value. If a few conditions are added to the query, AND logic is used to combine them.
Parameters:

  • name {String} - Name of a feature

and()

Adds Query Node to existing Node with AND logic, all subnodes and condition of which are combined with AND logic: node AND (cond1 AND cond2).
It returns:

  • {csQueryBuilder.Node}

not()

Adds Query Node to existing Node with AND NOT logic: node AND NOT(cond1 AND cond2)
It returns:

  • {csQueryBuilder.Node}

or()

Adds Query Node to existing Node with AND logic, all subnodes and condition of which are combined with OR logic: node AND (cond1 OR cond2)
It returns:

  • {csQueryBuilder.Node}

relation(direction, type)

Adds relation conditions to existing Node, which is used to search by related assets. Relation condition and subcondition are added using AND logic to existing node.
Parameters:

  • direction {String} - relation direction
  • type {String} - relation type

//all children for asset with id = assetId with relation type 'user.'
queryBuilder.relation('parent', 'user.').condition('censhare:asset.id', assetId);

It returns:

  • {csQueryBuilder.Node}

csQueryBuilder.Order

Allows to set order for result.

query.order().orderBy("censhare:asset.name", false);

orderBy(property, isAssending)

It sets an order for result assets

csQueryBuilder.View

Allows to set custom representation for assets.

var query = new csQueryBuilder(),
    view;
view = query.view();
view.setTransformation("censhare:cs5.query.asset.list");
view.setViewName(csViewNameResolver.getViewFromRowHeight("2"));

setTransformation(assetKey)

It sets a XSLT transformation asset for a query result, which will transform standard asset representation to a new one.
“censhare:cs5.query.asset.list” transformation is used in censhare to add required information for standard item renderers.

setViewName(viewName)

It sets workspace view name, which allows to specify additional data, which should be added into asset structure. Use csViewNameResolver to get appropriate viewName for asset representation.

setModelFormat(model)

It sets format of asset View Model. If no format is set, logical model of the asset will be returned. Set “update” as a model format to get asset Update model.

csChannelSniffer

The channel sniffer is a module that allows us to directly monitor the communication between the censhare Web client running on a browser and censhare server.

Reasons behind channel sniffer

Due to the fact that we use a WebSocket-based channel to communicate between the browser and the server, we are unable to directly monitor the traffic as it’s done in the traditional HTTP request-based applications (eg. using the ‘network’ tab from the browser’s developer tool). Moreover, the channeled data are compressed, which causes all webSocket-monitoring tools to display binary gibberish.

The channel sniffer allows us to log the data that are sent through the channel directly in browser’s console, just before they are compressed (requests) or inflated (responses).

The Sniffer can be used for example to monitor and check the validity of data that are sent/received from the application, without the need to interfere with the application code.

Using the channel sniffer

To use the channel sniffer, you need to:

  • activate the developer mode in censhare web (enter the user menu via clicking on the user info on top right corner of the screen, choose ‘About censhare web’ option and alt+click the close button in displayed about dialog).
  • activate the channel sniffer by clicking on the newly displayed developer menu (wrench icon), that appears next to the user info menu, and choose the ‘Toggle Channel Sniffer’ option.
  • open the console, in the browser’s developer tools
  • notice the request/response messages as you are using the application

Interpreting sniffer messages

ChannelSniffer

csService

createObservable<T>(initialValue: T): IObservable<T>;

It creates a csObservable instance with initial value of type T.

bindToScope(scope, propName: string, observable, mapper): void;

Binds csObservable or `csReader the scope. Optionally mapper function can be provided, which can transform csObservable or csReader value to any scope object.

For the given scope and property, the given observable is bind in a way that:
1. If the value of the observable changes, the value stored inside of the scope is updated.
2. If the scope is destroyed, the listener registered on the observable is automatically removed.

Hence, this method is the preferred way to automatically let a scope property continually reflect the value of an observable.
Please notice that the method checks if there is already a value stored in the property of the scope.

Parameters:

  • scope {ng.IScope} - scope
  • propName {string} - scope property name
  • observable {IReader |IObservable } - observable or reader
  • mapper {(reader: IReader ) => any} - optional mapper function. Receives an observable reader as a parameter.

csServices.bindToScope(scope, 'spreads', scope.dataModel.getActiveLayoutReader(), function (reader: IReader<Spread>) {
    const spreads = reader.getValue().spreads;
    const anotherSpreads: AnotherSpread[] = [];
    let newSpread: AnotherSpread;
 
    spreads.forEach(function (spread) {
        newSpread = new AnotherSpread(spread);
        anotherSpreads.push(newSpread);
    });
 
    return anotherSpreads;
});

createIt executesAsyncLastCallWrapper(a: any): any;

Create a wrapper for multiple calls of the function and executes the last call asynchronously.

If a function f is called with the invoke of angular multiple times, then for f’ = createIt executesAsyncLastCallWrapper(f) it holds calls and executes only the last one.
Parameters:

  • f {function} - any function

Return:

  • {function} - wrapped function

onDataChangedSignal = csServices.createIt executesAsyncLastCallWrapper(onDataChanged);

createIt executesAsyncLastCallWrapperNative(a: any): any;

Create a wrapper for multiple calls of the function and executes the last call asynchronously without triggering digest cycle for the document.

csUploadManagerService

This section describes the implementation of the file upload in censhare Web. The file upload is used to create assets or overwrite the Master file of an asset.

Services

There are two services available for the file upload:

  • csUploadsManager with the TypeScript class UploadManagerService
  • csFileUploadService with the TypeScript class FileUploadService

The actual upload process of both is identical, the differences are as follows:

csFileUploadService includes the duplicate check (wizard). This should be default for creating assets. Users can however disable it in their preferences so that the service will be skipped and delegated to csUploadsManager. This service implements the main upload logic.

Usage

  1. General usage with relation creation and duplicate check (remember: the duplicate check can be disabled by the user):
    Example: Create asset dialog

    const uploadFileConfig: any = {};
    if (context.relation) {
        uploadFileConfig.relation = {
           baseAssetId: context.parentAsset,
           relationType: context.relation.key,
           direction: this.csAssetUtil.invertDirection(context.relation.direction),
           sortable: false
        };
    }
     
    csFileUploadService.uploadFiles(files, uploadFileConfig);

  2. Doing the duplicate check manually without the wizard:
    Example: For a single file you can do the duplicate check without the wizard

    if (csFileUploadService.isDuplicateCheck()) {
        csFileUploadService.checkFileDuplicate(file).then(
            (result) => {
                if (result.assetId !== 0) {
                    // found duplicate, handle it
                } else {
                    // no duplicate, just upload
                    uploadNewFile();
                }
            }
        );
    } else {
        uploadNewFile();
    }
     
    const uploadNewFile = () => {
        return csFileUploadService.doUpload(file, { command: { key: cmdFromFile, params: params } });
    }

  3. Direct upload without duplicate check:

    Example: Upload Master file

    csUploadsManager.addNewUploads(files).then(
        (value) => {
            updateModel(value.assetId);
        },
        (error) => {
            csNotify.warning('csCommonTranslations.fileUploadFailed', error);
        },
        (progress) => {
            this.uploadProgressValue = progress;
        }
    );

  4. File size check before upload:
    checkMacimumSize is used to check a file’s size before upload, to ensure that its size is smaller than the specified limit in the system properties. Files with bigger size than the defined limit are excluded, a notification shows the names of the excluded files and the size limit.

    public checkMaximumSize(source: FileList | File | File[]): ng.IPromise<File[]> {
        const defer = this.$q.defer<File[]>();
        this.csSystemSettings.getFileUploadSettings().then((sysConfig: ISystemUploadSettings) => {
            const files = FileUploadService.toFileArray(source);
            const length = files.length - 1;
            const filesExceededLimitNames: string[] = [];
            for (let i = length; i >= 0; i--) {
                if (files[i].size > sysConfig.maximumSize) {
                    filesExceededLimitNames.push(files[i].name);
                    files.splice(i, 1);
                }
            }
            defer.resolve(files);
            if (filesExceededLimitNames.length > 0) {
                const maxSizeLimitMsg = this.csTranslate.instant('csUploadsManager.fileMaxSizeMessage',
                    {
                        filesNames: filesExceededLimitNames.join(','),
                        maxSize: this.csFileSizeService.convert(sysConfig.maximumSize)
                    });
                const maxSizeLimitTitle = this.csTranslate.instant('csUploadsManager.filesMaxSizeTitle');
                this.csNotify.warning(maxSizeLimitTitle, maxSizeLimitMsg);
            }
        });
        return defer.promise;
    }

    Example: Uploading files from the asset chooser

    public addNewUploadsWithFileChooser(multi: boolean, command?: string, options = {}): ng.IPromise<any> {
        const deferred = this.$q.defer();
     
        this.openFileChooser(multi, (files) => {
            this.checkMaximumSize(files).then((_files: File[]) => {
                if (_files.length > 0) {
                    deferred.resolve([this.addNewUploads(_files, command, options)]);
                }
            });
        });
     
        return deferred.promise;
    }

Technical background

Uploading a file or multiple files can be done via drag & drop or using a file chooser. The methods to upload accept a File object or a FileList objects which is an array like structure of files. Optionally, a config object can be passed which holds information about which CommandHandler should process the uploaded files and if relations to other assets should be created. See the section CommandHandlersbelow for more details.

New in this version the creation of temporary upload assets which holds the uploaded data while upload is in progress and are deleted after the final assets are created. This also enables a resume functionality of the upload which will be described in the client section.

The client side

On the client side we make use of the 3rd-party library resumable.js which is able to split a file into smaller pieces called chunks and upload those chunkseparate to the server. Furthermore resumable.js can ask the server if a chunk already exist and skip uploading this chunk. This implements the resume functionality. Also chunks can be uploaded in parallel.

If multiple files are added, they will be put into a queue. A configurable number of consumers will then create the uploads in parallel.

Options for the upload

You can provide optional options which are mainly used on server side for processing the uploaded data.

  • command: string - Id of a CommandHandler being used for uploaded data. Default is com.censhare.api.fileupload.NewAssetFromFile
  • relation: object - If you want to create a relation to another asset set the following properties
    • baseAssetId: number - Id of the asset to relate to
    • relationType: string - Key of the relation type or feature for asset-ref
    • direction: string - The relation direction [child | parent | feature | feature-reverse]

Configuration

The configuration defines the settings for resumable.js:

  • chuckSize - Size of each chunk in bytes
  • simultaneousUploadChunks - Number of chunks uploaded in parallel

There are as well the following general settings:

  • simultaneousUploadFiles - Number of files uploaded in parallel
  • chunkRetries - Number of retries if uploading a chunk fails before skipping the upload

The configuration is set on the system asset but can be overwritten by users in their preferences.

Hint for parallel uploads

Uploading in parallel can increase the total upload speed. Of course main limit is the bandwidth but multiple streams could result in using total bandwidth but this also depends on the latency. There’s no general rule how many simultaneous uploads are good but too many reduce the speed. A total number of 10 parallel connection should not be exceeded which mean 2 simultaneousUploadChunks and 5 simultaneousUploadFiles, or 3 and 3, etc.

As mentioned, there’s no general rule so I suggest using smaller numbers e.g. 2 and 2 but also try out what works best for you.

Server side

The uploaded uses the Java Servlet FileUploadServlet to process data. We’re using the apache commons upload library for streaming the data from client to the server. Because multiple uploaded can happen in parallel, there is the ResumableUploadService singleton to manage the creation, updating of temporary upload assets and calling the final CommandHandler to process the uploaded file. With the uploadId send by the client we can identify the temp upload asset and add the additional file chunks here as storage item. Every chunk will create a new storage item.

If the upload is interrupted (due to server or connection, an existing temp upload asset will be used and already uploaded chunks will be restored so the client don’t have to send them again.

Once all chunks are uploaded, the ResumableUploadService will execute the configured CommandHandler with an InputStream that concatenates the single chunks into a single stream.

CommandHandlers

There’re a few CommandHandlers in place to process the uploaded data.

  • com.censhare.api.fileupload.NewAssetFromFile which is the default one for creating assets from files
  • com.censhare.api.dam.assetmanagement.replacemasterstorageitem to replace the storage item of existing asset
  • com.censhare.api.command.server.upload to upload files and provide them to classic server actions

EventEmitter

The EventEmitter class is defined and exposed by the csEvents module:

import {EventEmitter} from '<relative-path>/csEvents/event-emitter';

All objects that emit events are instances of the EventEmitter class. These objects expose an .on() and .once methods, that allow one or more functions to be registered to named events emitted by the EventEmitter. Event names should typically be camel-cased strings.

When the EventEmitter emits an event, all of the functions registered on that specific event are called synchronously.

Registering and triggering of the event

The following example shows a simple EventEmitter instance with a single listener registered to an event. The .on() method is used to register listeners, while the .emit() method is used to trigger the event by specifying the event name as a first function argument, and will return a boolean value whether an event was triggered or not.

const emitter: EventEmitter<string> = new EventEmitter();
 
emitter.on('eventName', () => {
     console.log('Event "eventName" was triggered.');
});
 
emitter.emit('eventName'); // Returns true - event 'eventName' triggered

The EventEmitter calls all event listeners synchronously in the order in which they were registered, so it is important to ensure the proper sequence of events in order to avoid race conditions or logic errors.

Passing arguments to the listeners

The .emit() method allows an arbitrary set of arguments to be passed to the event listeners. The first argument is always event name, while all following arguments will be used as arguments of all listeners registered to the event.

const emitter: EventEmitter<string> = new EventEmitter();
 
emitter.on('eventName', (firstArgument, secondArgument) => {
     console.log(firstArgument, secondArgument); // Prints: 1 2
});
 
emitter.emit('eventName', 1, 2); // Returns true - event 'eventName' triggered

Handling event only once

When a listener is registered using the .on() method, that listener will be invoked every time the event is emitted.
Using the .once() method, it is possible to register a listener that is called only once.

const emitter: EventEmitter<string> = new EventEmitter();
 
emitter.once('eventName', () => {
     console.log('Event "eventName" was triggered.');
});
 
emitter.emit('eventName'); // Returns true - event 'eventName' triggered
emitter.emit('eventName'); // Returns false - event 'eventName' has no registered listeners

Registering listener with context

When a listener is a function without context - prototype, access to this will fail with undefined exception.

class TestClass {
    private emitter: EventEmitter<string> = new EventEmitter();
    private num: number = 0;
 
    constructor() {
        this.emitter.on('eventName', this.eventListenerNoContext);
        this.emitter.emit('eventName'); // Uncaught TypeError: Cannot read property 'num' of undefined
    }
 
    eventListenerNoContext(): void {
        this.num++;
        console.log(this.num);
    }
}

To fix this error either register the lambda function:

class TestClass {
    private emitter: EventEmitter<string> = new EventEmitter();
    private num: number = 0;
 
    constructor() {
        this.emitter.on('eventName', this.eventListenerWithContext);
        this.emitter.emit('eventName');
    }
 
    eventListenerWithContext = (): void => {
        this.num++;
        console.log(this.num);
    };
}

Or use a third argument on the .on() method, to specify your context:

class TestClass {
    private emitter: EventEmitter<string> = new EventEmitter();
    private num: number = 0;
 
    constructor() {
        this.emitter.on('eventName', this.eventListenerNoContext, this);
        this.emitter.emit('eventName');
    }
 
    eventListenerNoContext(): void {
        this.num++;
        console.log(this.num);
    }
}

Unregistering the event listener

To unregister the event listener properly, the .off() method is used.

Method

Description

emitter.off()It will unregister all event listeners
emitter.off('eventName')It will unregister all event listeners from a specific event (e.g. eventName)
emitter.off('eventName', listener)It will unregister the event listener from the event that is matching the provided function
emitter.off('eventName', listener, context)It will unregister the listeners that are matching the event name, function, context
emitter.off('eventName', listener, context, once)It will unregister the listeners that are matching the event name, function, context, once
Unregister all event listeners

emitter.on('eventName1', eventListener1); // Will be unregistered
emitter.on('eventName2', eventListener2); // Will be unregistered
 
emitter.off();

Unregister event listeners from one event

emitter.on('eventName1', eventListener1); // Will be unregistered
emitter.on('eventName2', eventListener2);
 
emitter.off('eventName1');

Unregister only one listener from the event

emitter.on('eventName1', eventListener1); // Will be unregistered
emitter.on('eventName2', eventListener2);
 
emitter.off('eventName1', eventListener1);

Unregister only event listener matching to provided context

emitter.on('eventName1', eventListener1, context1); // Will be unregistered
emitter.on('eventName1', eventListener1, context2);
 
emitter.off('eventName1', eventListener1, context1);

Unregister only event listener matching registered with .once()

emitter.on('eventName1', eventListener1, context1);
emitter.once('eventName1', eventListener1, context1); // Will be unregistered
 
emitter.off('eventName1', eventListener1, context1, true);

Strict event handling

By using typescript generics we can as well lock the events we want to allow, and typescript will throw an error for any other event.

type allowedTypes = 'eventName1' | 'eventName2';
const emitter = new EventEmitter<allowedTypes>();
 
emitter.on('eventName1', eventListener); // OK
emitter.on('eventName2', eventListener); // OK
emitter.on('eventName3', eventListener); // Error: TS2345
 
emitter.emit('eventName1'); // OK
emitter.emit('eventName2'); // OK
emitter.emit('eventName3'); // Error: TS2345

Methods

Method

Description

getEventNames()It returns all events with registered event listeners
getListeners(event: string)It returns all listeners registered to the provided event
exist(event: string)It returns a boolean value on whether the listeners are registered to the provided event

Dialogs

csAssetCreateDialog

  • Extended version of csAssetChooserDialog
  • Allows user to choose or create a new asset from 1 dialog
  • Contains tabs:
    • New asset - create asset using metadata dialog
    • Asset from template
    • Asset from file
    • Existing asset
  • Provides possibility to hide tabs and set default one

Usage

  • Add csAssetCreateDialog module to dependencies
  • Inject csAssetCreateDialog to a component
  • Open the csAssetCreateDialog:

const dialogContext: any = {
    title: 'csAssetCreateRelationBehavior.titleRelation',
    defaultTemplate: properties.defaultTemplate,
    asset: contextAsset,
    addAssetTab: true,
    relation: relation,
    defaultTab: 3,
    parentAsset: csAssetUtil.getAssetIdFromAssetRef(contextAsset.self)
};
 
csAssetCreateDialog.open(dialogManager, dialogContext).then((result: IcsAssetCreateDialogResultData) => {...});

context

Context object can contain following properties:

  • title - defines the dialog title
  • addAssetFromTemplateTab - flag for displaying the “Asset from template” tab.
  • addNewAssetTab - flag for displaying the “New asset” tab. By default is true.
  • addUploadTab - flag for displaying the “Asset from file” tab. By default is true.
  • addAssetTab - flag for displaying the “Existing asset” tab. By default is false.
  • defaultTab - active tab index, its default value is 0.
  • defaultTemplate - the resource key of the Asset Metadata Dialog that should be shown in the New asset tab.
  • targetAssetType - restricts the dropdown and show dialog to a specific asset type.

const dialogContext = {
    title: 'csCommonTranslations.newAsset',
    defaultTemplate: 'censhare:metadata.default.asset.template'
};
 
csAssetCreateDialog.open(globalDialogManagerPromise, dialogContext).then((result: IcsAssetCreateDialogResultData) => {...});

Additionall, csAssetCreateDialog context can contain properties for the csAssetChooserDialog which allows to setup how the Existing asset tab will look like.

csAssetChooserDialog

  • Used to choose one or more assets
  • Allows to use filters and search
  • Can contain configurable tabs
  • Returns an array‚ of selected assets in resolved promise

Usage

  • Add csAssetChooserDialog module to dependencies
  • Inject csAssetChooserDialog to a component
  • Open the csAssetChooserDialog:

const dialogContext: IcsAssetCreateDialogProperties = {
    title: 'csAssetCreateRelationBehavior.titleRelation',
    defaultTemplate: properties.defaultTemplate,
    asset: contextAsset,
    addAssetTab: true,
    relation: relation,
    defaultTab: 3,
    parentAsset: csAssetUtil.getAssetIdFromAssetRef(contextAsset.self)
};
 
csAssetCreateDialog.open(dialogManager, dialogContext).then((result: IcsAssetCreateDialogResultData) => {...});

context

Context object can contain following properties:

  • singleSelect - if single selection mode is active, false by default
  • refinements - array of defined tabs. Could be configured with a query or predefined.
    • Predefined tabs: “all”, “related_assets”, “currently_editing”, “tasks”, “last_created”, “last_edited”, “pinboard”
    • Custom tab is an object, which contains label, key and a query
    • It’s possible to mix custom and predefined tabs
    • If the property refinements is not defined, it is initialized by the AssetChooser
    • all is added by default

context.refinements = ["related_assets", {
    label: "FlowersByName",
    key: "flowersByName",
    query: {
        condition: {
             name: "censhare:asset.name",
            value: "flower"
        }
    }
}]

  • filter - a query, which will be applied to all tabs

queryBuilder = new csQueryBuilder();
and = queryBuilder.and();
and.not().condition('censhare:storage_item.key', 'master');
and.not().condition('censhare:asset.type', 'group.');
query = queryBuilder.build();
 
csAssetChooserDialog(pageInstance.getDialogManager(), 'csLayoutEditorWidget.selectAsset', {
    singleSelect: true,
    filter: query,
    refinements: []
}).then(function (assets) {
    ...
});

  • behaviors - list of behaviors which should appear for asset, empty by default
  • allowedFor - asset type filter, simplified way to set type filter for the dialog.

context.allowedFor = ["task.*"]

  • template - boolean flag, if only templates (‘censhare:asset-flag’ = ‘is-template’) should appear in the dialog

Observables

csObservable

The csObservable is an implementation of Observer design pattern.

The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. It is mainly used to implement distributed event handling systems.

To use csObservable in your code:

  • include csServices as a dependency for your module
  • include censhare type definitions in your module to use csServices interfaces in TypeScript code:

import * as censhare from "censhare";

  • Use csServices.createObservable(initialValue): IObservable<T> to create an observable:

Parameters: initialValue {T} - initial value of csObservable
Returns: IObservable

class someControllerClass {
    static $inject: string[] = [..., 'csServices'];
 
    constructor(..., csServices: csServices.IcsServicesService) {
        csServices.IObservable<string> someObservable;
        someObservable = csServices.createObservable("Some initial value");
        ...
    }
}

  • csObservable implements interface csServices.IObservable
  • Read more about registering listeners in csReader

Methods

getValue(): T

Returns current value of observable.

setValue(value: T): IObservable<T>

Sets a new value for observable. If the value is different than previous one, all observers are notified.

getReader(): IReader<T>

Returns reader for observable value, which is instance of csReader.

notify(): void

Notify all listeners even if value wasn’t changed.

destroy(): void

Destroy observable.

// set a new value
someObservable.setValue("Another value");
 
// get current value
String currentValue = someObservable.getValue();
 
// notify all listeners
someObservable.notify();
 
// add a new listener and notify
someObservable.getReader().registerChangeListenerAndFire(function(value) {...}, $scope);
 
// add a new listener
someObservable.getReader().registerChangeListener(function(value) {...}, $scope);

registerChangeListener() vs registerNativeChangeListener()

Observables have 2 types of listeners:

  • listeners which are registered by the registerChangeListener() listener
  • nativeListeners which are registered by the registerNativeChangeListener() listener

If an observable has listeners.length > 0, then it will trigger the $apply() for the document.
If an observable has only nativeListeners, then no digest cycle will be performed.

As $apply() is a very expensive operation towards performance, make sure to avoid using the registerChangeListener() when there is no need to update the DOM. When there is need to update only a part of the DOM, use csServides.bindToScope(). It will bind an observable or a reader of the observable to the scope object:

csServices.bindToScope($scope, 'data', dataObservable);
csServices.bindToScope($scope, 'data', dataReader);

csReader

csReader provides possibility to read and listen the value of an csObservable.

getValue(): T;

Get the current value of a csObservable.

registerChangeListener(listener: (value: T) => void, destroyable?: any): void;

It registers the given callback as a listener. If the second parameter is passed, the register will be automatically removed if the destroyable is destroyed, e.g. if the scope is destroyed or for a pageInstance, if the instance is closed.

Note: When csObservable will get a new value, it will trigger $apply(), so update the whole document, if at least one listener was registered.

Prefer to use registerNativeChangeListener() when $apply() is not needed. To trigger local updates for changed value use csServides.bindToScope() which will bind an observable or reader to the scope:

csServices.bindToScope($scope, 'data', dataObservable);
csServices.bindToScope($scope, 'data', dataReader);

Read more about csServides.bindToScope()

Parameters:

  • listener - callback to register
  • destroyable - destroyable object to remove listener automatically, when destroyable will be destroyed

registerChangeListenerAndFire(listener: (value: T) => void, destroyable?: any): void;

It registers the given callback as a listener and sends notification immediately. The difference with registerChangeListener() is that after registering the callback immediately receives a notification with current value of the observable.

registerNativeChangeListener(listener: (value: T) => void, destroyable?: any): void;

It registers the given callback as a native listener. The difference with registerChangeListener() is that csObservable with native listeners only will not trigger digest cycle update for the document.

registerNativeChangeListenerAndFire(listener: (value: T) => void, destroyable?: any): void;

It registers the given callback as a native listener and sends notification immediately.

unregisterChangeListener(listener: (value: T) => void): void;

It unregisters a registered listener.

unregisterNativeChangeListener(listener: (value: T) => void): void;

It unregisters a registered native listener.