Creating New Functions, Widgets, Card Components, DataTypes, and Plugins

Functions, Widgets, Card Components, DataTypes, and Plugins have overlapping functionality and architecture. These constructs are used to add extra functionality to Arches upon data entry or data visualization.

At a high level, if you’re trying to add a custom interface for entering business data, you’ll use a Widget. If you need to enter and display a type of data unknown to Arches, you’ll need a new DataType to go with your Widget. If you need sophisticated side effects or to perform additional computation based on the data entered, it’s time for a Function.

Note

To develop these components, you’ll need familiarity with Django templates, intermediate skills in the Python programming language, and some basic familiarity with the Knockout.js front-end framework.

Functions

Functions are the most powerful extension to Arches. Functions associated with a Resource are called during various CRUD operations, and have access to any server-side model. Proficient Python/Django developers will find few limitations extending an Arches Project with Functions.

Function must be created, registered, and then associated with a Resource Model.

Creating a Function

A Function comprises three separate files, which should be seen as front-end/back-end complements. On the front-end, you will need a component made from a Django HTML template and JavaScript pair, which should share the same basename.

In your Project, these files must be placed like so:

/myproject/myproject/media/js/views/components/functions/spatial_join.js /myproject/myproject/templates/views/components/functions/spatial_join.htm

The third file is a Python file which contains a dictionary telling Arches some important details about your Function, as well as its main logic.

/myproject/myproject/functions/spatial_join.py

Note

As in the example above, its advisable that all of your files share the same basename. (If your Function is moved into a Package, this is necessary.) A new Project should have an example function in it whose files you can copy to begin this process.

Defining the Function’s Details

The first step in creating a function is defining the details that are in the top of your Function’s .py file.

details = {
    'name': 'Sample Function',
    'type': 'node',
    'description': 'Just a sample demonstrating node group selection',
    'defaultconfig': {"selected_nodegroup":""},
    'classname': 'SampleFunction',
    'component': 'views/components/functions/sample-function'
}
name:Required Name is used to unregister a function, and shows up in the fn list command.
type:Required As of version 4.2, this should always be set to node
description:Optional Add a description of what your Function does.
defaultconfig:Required A JSON object with any configuration needed to serve your function’s logic
classname:Required The name of the python class that holds this Function’s logic.
component:Required Canonical path to html/js component.

More about the defaultconfig field

Any configuration information you need your Function to access can be stored here. If your function needs to calculate something based on the value of an existing Node, you can refer to it here. Or, if you want your Function to e-mail an administrator whenever a specific node is changed, both the Node ID and the email address to be used are good candidates for storage in the defaultconfig dictionary.

The defaultconfig field serves both as a default, and as your user-defined schema for your function’s configuration data. Your front-end component for the function will likely collect some of this configuration data from the user and store it in the config attribute of the pertinent FunctionXGraph.

Writing your Function Logic

In your Function’s Python code, you have access to all your server-side models. You’re basically able to extend Arches in any way you please. You may want to review the Data Model documentation.

Function Hooks

Your function needs to extend the BaseFunction class. Depending on what you are trying to do, you will need to implement the get, save, delete, on_import, and/or after_function_save methods.

class MyFunction(BaseFunction):

    def get(self):
        raise NotImplementedError

    def save(self, tile, request):
        raise NotImplementedError

    def delete(self, tile, request):
        raise NotImplementedError

    def on_import(self, tile):
        raise NotImplementedError

    def after_function_save(self, functionxgraph, request):
        raise NotImplementedError

Note

Not all of these methods are called in the current Arches software. You can also leave any of them unimplemented, and the BaseFunction class will raise a NotImplementedError for you. Arches is designed to gracefully ignore these exceptions for functions.

A detailed description of current functionality is below.

save and delete

The Tile object will look up all its Graph’s associated Functions upon being saved. Before writing to the database, it calls each function’s save method, passing itself along with the Django Request object. This is likely where the bulk of your function’s logic will reside.

The Tile object similarly calls each of its graph’s functions’ delete methods with the same parameters. Here, you can execute any cleanup or other desired side effects of a Tile’s deletion. Your delete implementation will have the same signature as save.

after_function_save

The Graph view passes a FunctionXGraph object to after_function_save, along with the request.

The FunctionXGraph object has a config attribute which stores that instance’s version of the defaultconfig dictionary. This is a good opportunity, for example, to programmatically manipulate the Function’s configuration based on the Graph or any other server-side object.

You can also write any general logic that you’d like to fire upon the assignment of a Function to a Resource.

on_import

The import module calls on_import if the file format is a JSON-formatted Arches file, and passes an associated Tile object.

CSV imports do not call this hook.

The UI Component

Having implemented your function’s logic, it’s time to develop the front-end components required to associate it with Resources and provide any configuration data.

The component you develop here will be rendered in the Resource Manager when you associate the function with a Resource, and this is where you’ll put any forms or other UI artifacts used to configure the Function.

Developing your Function’s UI component is very similar to developing Widgets. More specific guidelines are in progress, but for now, refer to the sample code in your project’s templates/views/components/functions/ directory, and gain a little more insight from the templates/views/components/widgets/ directory. The complementary JavaScript examples will be located in media/js/views/components/functions/ and media/js/views/components/widgets directories.

Registering Functions

First, list the names of functions you already have registered:

(ENV)$ python manage.py fn list

Now you can register your new function with

(ENV)$ python manage.py fn register-source <path to your function's .py file>

For example:

(ENV)$ python manage.py fn register --source /Documents/projects/mynewproject/mynewproject/functions/sample_function.py

Now navigate to the Function Manager in the Arches Designer to confirm that your new function is there and functional. If it’s not, you may want to unregister your function, make additional changes, and re-register it. To unregister your function, simply run

(ENV)$ python manage.py fn unregister --name 'Sample Function'

All commands are listed in Command Line Reference - Function Commands.

Widgets

Widgets allow you to customize how data of a certain DataType is entered into Arches, and further customize how that data is presented in Reports. You might have several Widgets for a given DataType, depending on how you want the Report to look or to match the context of a certain Resource.

Widgets are primarily a UI artifact, though they are closely tied to their underlying DataType.

To develop a custom Widget, you’ll need to write three separate files, and place them in the appropriate directories. For the appearance and behavior of the Widget, you’ll need a component made of a Django template and JavaScript file placed like so:

project_name/templates/views/components/widgets/sample-widget.htm project_name/media/js/views/components/widgets/sample-widget.js

To register and configure the Widget, you’ll need a JSON configuration file:

project_name/widgets/sample-widget.json

Configuring your Widget

To start, here is a sample Widget JSON file:

{
    "name": "sample-widget",
    "component": "views/components/widgets/sample-widget",
    "defaultconfig": {
        "x_placeholder":"Longitude",
        "y_placeholder":"Latitude"
    },
    "helptext": null,
    "datatype": "sample-datatype"
}

The most important field here is the datatype field. This controls where your Widget will appear in the Arches Resource Designer. Nodes each have a DataType, and Widgets matching that DataType will be available when you’re designing your Cards. The value must match an existing DataType within Arches.

You can also populate the defaultconfig field with any configuration data you wish, to be used in your Widget’s front-end component.

Designing Your Widget

Your Widget’s template needs to include three Django template “blocks” for rendering the Widget in different contexts within Arches. These blocks are called form, config_form, and report. As you might guess from their names, form is rendered when your Widget appears on a Card for business data entry, config_form is rendered when you configure the Widget on a card when designing a Resource, and report controls how data from your Widget is presented in a Report.

Here is an example:

{% extends "views/components/widgets/base.htm" %}
{% load i18n %}

{% block form %}
<div class="row widget-wrapper">
    <div class="form-group">
        <label class="control-label widget-input-label" for="" data-bind="text:label"></label>
        <div class="col-xs-12">
            <input type="number" data-bind="textInput: x_value, attr: {placeholder: x_placeholder}" class="form-control input-lg widget-input" style="margin-bottom: 5px">
        </div>
        <div class="col-xs-12">
            <input type="number" data-bind="textInput: y_value, attr: {placeholder: y_placeholder}" class="form-control input-lg widget-input" style="margin-bottom: 5px">
        </div>
        <div class="col-xs-12">
            <input type="text" data-bind="textInput: srid" class="form-control input-lg widget-input" style="margin-bottom: 5px">
        </div>
        <div class="col-xs-12">
            <label class="control-label widget-input-label" for="">Preview</label>
            <input disabled type="text" data-bind="textInput: preview" class="form-control input-lg widget-input">
        </div>
    </div>
</div>
{% endblock form %}

{% block config_form %}
<div class="control-label">
    {% trans "X Coordinate Placeholder" %}
</div>
<div class="col-xs-12 crud-widget-container">
    <input type="" placeholder="{% trans "Placeholder" %}" id="" class="form-control input-md widget-input" data-bind="textInput: x_placeholder">
</div>
<div class="control-label">
    {% trans "Y Coordinate Placeholder" %}
</div>
<div class="col-xs-12 crud-widget-container">
    <input type="" placeholder="{% trans "Placeholder" %}" id="" class="form-control input-md widget-input" data-bind="textInput: y_placeholder">
</div>
{% endblock config_form %}

{% block report %}
<dt data-bind="text: label"></dt>
<dd>
  <div style='margin-bottom:2px' data-bind="text: value"></div>
</dd>
{% endblock report %}

To pull it all together, you’ll need to write a complementary JavaScript file. The Arches UI uses Knockout.js, and the best way to develop your Widget in a compatible way is to write a Knockout component with a viewModel corresponding to your Widget’s view (the Django template).

Here is an example, continuing with our sample-widget:

define(['knockout', 'underscore', 'viewmodels/widget'], function (ko, _, WidgetViewModel) {
    /**
    * registers a text-widget component for use in forms
    * @function external:"ko.components".text-widget
    * @param {object} params
    * @param {string} params.value - the value being managed
    * @param {function} params.config - observable containing config object
    * @param {string} params.config().label - label to use alongside the text input
    * @param {string} params.config().placeholder - default text to show in the text input
    */
    return ko.components.register('sample-widget', {
        viewModel: function(params) {
            params.configKeys = ['x_placeholder','y_placeholder'];
            WidgetViewModel.apply(this, [params]);
            var self = this;
            if (this.value()) {
                var coords = this.value().split('POINT(')[1].replace(')','').split(' ')
                var srid = this.value().split(';')[0].split('=')[1]
                this.x_value = ko.observable(coords[0]);
                this.y_value = ko.observable(coords[1]);
                this.srid = ko.observable('4326');
            } else {
                this.x_value = ko.observable();
                this.y_value = ko.observable();
                this.srid = ko.observable('4326');
            };

            this.preview = ko.pureComputed(function() {
                var res = "SRID=" + this.srid() + ";POINT(" + this.x_value() + " " + this.y_value() + ")"
                this.value(res);
                return res;
            }, this);
        },
        template: { require: 'text!templates/views/components/widgets/sample-widget.htm' }
    });
});

Registering your Widget

After placing your Django template and JavaScript files in their respective directories, you are now ready to register your Widget:

python manage.py widget register --source /Documents/projects/mynewproject/mynewproject/widgets/sample-widget.json

The command will confirm your Widget has been registered, and you can also see it with:

python manage.py widget list

If you make an update to your Widget, you can load the changes to Arches with:

python manage.py widget update --source /Documents/projects/mynewproject/mynewproject/widgets/sample-widget.json

All the Widget commands are detailed in Command Line Reference - Widget Commands.

DataTypes

A DataType defines a type of business data. DataTypes are associated with Nodes and Widgets. When you are designing your Cards, the Widgets with the same DataType as the Node you are collecting data for will be available. In your Branches, each Node with a DataType will honor the DataType configuration you specify when you create it.

The simplest (non-configurable, non-searchable) DataTypes consist of a single Python file. If you want to provide Node-specific configuration to your DataType (such as whether to expose a Node with that DataType to Advanced Search or how the data is rendered), you’ll also develop a UI component comprising a Django template and JavaScript file.

In your Project, these files must be placed accordingly:

Optional Configuration Component:

/myproject/myproject/media/js/views/components/datatypes/sample_datatype.js /myproject/myproject/templates/views/components/datatypes/sample_datatype.htm

DataType File:

/myproject/myproject/datatypes/sample_datatype.py

To begin, let’s examine the sample-datatype included with Arches:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from arches.app.datatypes.base import BaseDataType
from arches.app.models import models
from arches.app.models.system_settings import settings

sample_widget = models.Widget.objects.get(name="sample-widget")

details = {
    "datatype": "sample-datatype",
    "iconclass": "fa fa-file-code-o",
    "modulename": "datatypes.py",
    "classname": "SampleDataType",
    "defaultwidget": sample_widget,
    "defaultconfig": {"placeholder_text": ""},
    "configcomponent": "views/components/datatypes/sample-datatype",
    "configname": "sample-datatype-config",
    "isgeometric": False,
    "issearchable": False,
}


class SampleDataType(BaseDataType):
    def validate(self, value, row_number=None, source=None):
        errors = []
        try:
            value.upper()
        except:
            errors.append(
                {
                    "type": "ERROR",
                    "message": "datatype: {0} value: {1} {2} {3} - {4}. {5}".format(
                        self.datatype_model.datatype, value, row_number, source, "this is not a string", "This data was not imported.",
                    ),
                }
            )
        return errors

    def append_to_document(self, document, nodevalue, nodeid, tile):
        document["strings"].append({"string": nodevalue, "nodegroup_id": tile.nodegroup_id})

    def transform_export_values(self, value, *args, **kwargs):
        if value != None:
            return value.encode("utf8")

    def get_search_terms(self, nodevalue, nodeid=None):
        terms = []
        if nodevalue is not None:
            if settings.WORDS_PER_SEARCH_TERM == None or (len(nodevalue.split(" ")) < settings.WORDS_PER_SEARCH_TERM):
                terms.append(nodevalue)
        return terms

    def append_search_filters(self, value, node, query, request):
        try:
            if value["val"] != "":
                match_type = "phrase_prefix" if "~" in value["op"] else "phrase"
                match_query = Match(field="tiles.data.%s" % (str(node.pk)), query=value["val"], type=match_type,)
                if "!" in value["op"]:
                    query.must_not(match_query)
                    query.filter(Exists(field="tiles.data.%s" % (str(node.pk))))
                else:
                    query.must(match_query)
        except KeyError, e:
            pass

Writing Your DataType

Your DataType needs, at minimum, to implement the validate method. You’re also likely to implement the transform_import_values or transform_export_values methods. Depending on whether your DataType is spatial, you may need to implement some other methods as well. If you want to expose Nodes of your DataType to Advanced Search, you’ll also need to implement the append_search_filters method.

You can get a pretty good idea of what methods you need to implement by looking at the BaseDataType class in the Arches source code located at arches/app/datatypes/base.py and below:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import json
from django.core.urlresolvers import reverse
from arches.app.models import models

class BaseDataType(object):

    def __init__(self, model=None):
        self.datatype_model = model

    def validate(self, value, row_number=None, source=None):
        return []

    def append_to_document(self, document, nodevalue, nodeid, tile):
        """
        Assigns a given node value to the corresponding key in a document in
        in preparation to index the document
        """
        pass

    def after_update_all(self):
        """
        Refreshes mv_geojson_geoms materialized view after save.
        """
        pass

    def transform_import_values(self, value, nodeid):
        """
        Transforms values from probably string/wkt representation to specified
        datatype in arches
        """
        return value

    def transform_export_values(self, value, *args, **kwargs):
        """
        Transforms values from probably string/wkt representation to specified
        datatype in arches
        """
        return value

    def get_bounds(self, tile, node):
        """
        Gets the bounds of a geometry if the datatype is spatial
        """
        return None

    def get_layer_config(self, node=None):
        """
        Gets the layer config to generate a map layer (use if spatial)
        """
        return None

    def should_cache(self, node=None):
        """
        Tells the system if the tileserver should cache for a given node
        """
        return False

    def should_manage_cache(self, node=None):
        """
        Tells the system if the tileserver should clear cache on edits for a
        given node
        """
        return False

    def get_map_layer(self, node=None):
        """
        Gets the array of map layers to add to the map for a given node
        should be a dictionary including (as in map_layers table):
        nodeid, name, layerdefinitions, isoverlay, icon
        """
        return None

    def clean(self, tile, nodeid):
        """
        Converts '' values to null when saving a tile.
        """
        if tile.data[nodeid] == '':
            tile.data[nodeid] = None

    def get_map_source(self, node=None, preview=False):
        """
        Gets the map source definition to add to the map for a given node
        should be a dictionary including (as in map_sources table):
        name, source (json)
        """
        tileserver_url = reverse('tileserver')
        if node is None:
            return None
        source_config = {
            "type": "vector",
            "tiles": ["%s/%s/{z}/{x}/{y}.pbf" % (tileserver_url, node.nodeid)]
        }
        count = None
        if preview == True:
            count = models.TileModel.objects.filter(data__has_key=str(node.nodeid)).count()
            if count == 0:
                source_config = {
                    "type": "geojson",
                    "data": {
                        "type": "FeatureCollection",
                        "features": [
                            {
                                "type": "Feature",
                                "properties": {
                                    "total": 1
                                },
                                "geometry": {
                                    "type": "Point",
                                    "coordinates": [
                                        -122.4810791015625,
                                        37.93553306183642
                                    ]
                                }
                            },
                            {
                                "type": "Feature",
                                "properties": {
                                    "total": 100
                                },
                                "geometry": {
                                    "type": "Point",
                                    "coordinates": [
                                        -58.30078125,
                                        -18.075412438417395
                                    ]
                                }
                            },
                            {
                                "type": "Feature",
                                "properties": {
                                    "total": 1
                                },
                                "geometry": {
                                    "type": "LineString",
                                    "coordinates": [
                                        [
                                            -179.82421875,
                                            44.213709909702054
                                        ],
                                        [
                                            -154.16015625,
                                            32.69486597787505
                                        ],
                                        [
                                            -171.5625,
                                            18.812717856407776
                                        ],
                                        [
                                            -145.72265625,
                                            2.986927393334876
                                        ],
                                        [
                                            -158.37890625,
                                            -30.145127183376115
                                        ]
                                    ]
                                }
                            },
                            {
                                "type": "Feature",
                                "properties": {
                                    "total": 1
                                },
                                "geometry": {
                                    "type": "Polygon",
                                    "coordinates": [
                                        [
                                            [
                                                -50.9765625,
                                                22.59372606392931
                                            ],
                                            [
                                                -23.37890625,
                                                22.59372606392931
                                            ],
                                            [
                                                -23.37890625,
                                                42.94033923363181
                                            ],
                                            [
                                                -50.9765625,
                                                42.94033923363181
                                            ],
                                            [
                                                -50.9765625,
                                                22.59372606392931
                                            ]
                                        ]
                                    ]
                                }
                            },
                            {
                                "type": "Feature",
                                "properties": {
                                    "total": 1
                                },
                                "geometry": {
                                    "type": "Polygon",
                                    "coordinates": [
                                        [
                                            [
                                                -27.59765625,
                                                -14.434680215297268
                                            ],
                                            [
                                                -24.43359375,
                                                -32.10118973232094
                                            ],
                                            [
                                                0.87890625,
                                                -31.653381399663985
                                            ],
                                            [
                                                2.28515625,
                                                -12.554563528593656
                                            ],
                                            [
                                                -14.23828125,
                                                -0.3515602939922709
                                            ],
                                            [
                                                -27.59765625,
                                                -14.434680215297268
                                            ]
                                        ]
                                    ]
                                }
                            }
                        ]
                    }
                }
        return {
            "nodeid": node.nodeid,
            "name": "resources-%s" % node.nodeid,
            "source": json.dumps(source_config),
            "count": count
        }

    def get_pref_label(self, nodevalue):
        """
        Gets the prefLabel of a concept value
        """
        return None

    def get_display_value(self, tile, node):
        """
        Returns a list of concept values for a given node
        """
        return unicode(tile.data[str(node.nodeid)])

    def get_search_terms(self, nodevalue, nodeid=None):
        """
        Returns a nodevalue if it qualifies as a search term
        """
        return []

    def append_search_filters(self, value, node, query, request):
        """
        Allows for modification of an elasticsearch bool query for use in
        advanced search
        """
        pass

    def handle_request(self, current_tile, request, node):
        """
        Updates files
        """
        pass

the validate method

Here, you write logic that the Tile model will use to accept or reject a Node’s data before saving. This is the core implementation of what your DataType is and is not.

The validate method returns an array of errors. If the array is empty, the data is considered valid. You can populate the errors array with any number of dictionaries with a type key and a message key. The value for type will generally be ERROR, but you can provide other kinds of messages.

the append_search_filters method

In this method, you’ll create an ElasticSearch query Nodes matching this datatype based on input from the user in the Advanced Search screen. (You design this input form in your DataType’s front-end component.)

Arches has its own ElasticSearch query DSL builder class. You’ll want to review that code for an idea of what to do. The search view passes your DataType a Bool() query from this class, which you call directly. You can invoke its must, filter, should, or must-not methods and pass complex queries you build with the DSL builder’s Match class or similar. You’ll execute this search directly in your append_search_filters method.

In-depth documentation of this part is planned, but for now, look at the core datatypes located in Arches’ source code for examples of the approaches you can take here.

Note

If you’re an accomplished Django developer, it should also be possible to use Elastic’s own Python DSL builder in your Project to build the complex search logic you’ll pass to Arches’ Bool() search, but this has not been tested.

Configuring your DataType

You’ll need to populate the details dictionary to configure your new DataType.

details = {
    "datatype": "sample-datatype",
    "iconclass": "fa fa-file-code-o",
    "modulename": "datatypes.py",
    "classname": "SampleDataType",
    "defaultwidget": sample_widget,
    "defaultconfig": {"placeholder_text": ""},
    "configcomponent": "views/components/datatypes/sample-datatype",
    "configname": "sample-datatype-config",
    "isgeometric": False,
    "issearchable": False,
datatype:Required The name of your datatype. The convention in Arches is to use kebab-case here.
iconclass:Required The FontAwesome icon class your DataType should use. Browse them here.
modulename:Required This should always be set to datatypes.py unless you’ve developed your own Python module to hold your many DataTypes, in which case you’ll know what to put here.
classname:Required The name of the Python class implementing your datatype, located in your DataType’s Python file below these details.
defaultwidget:Required The default Widget to be used for this DataType.
defaultconfig:Optional You can provide user-defined default configuration here.
configcomponent:
 Optional If you develop a configuration component, put the fully-qualified name of the view here. Example: views/components/datatypes/sample-datatype
configname:Optional The name of the Knockout component you have registered in your UI component’s JavaScript file.
isgeometric:Required Used by the Arches UI to determine whether to create a Map Layer based on the DataType, and also for caching. If you’re developing such a DataType, set this to True.
issearchable:Optional Determines if the datatype participates in advanced search. The default is false.

Important

configcomponent and configname are required together.

Developing the Configuration Component

Your component JavaScript file should register a Knockout component with your DataType’s configname. This component should be an object with two keys: viewModel, and template

The value for viewModel should be a function where you put the logic for your template. You’ll be setting up Knockout observable and computed values tied to any form elements you’ve developed to collect Advanced Search or Node-level configuration information from the user.

The value for template should be another object with the key require, and the value should be text!datatype-config-templates/<your-datatype-name>. Arches will know what to do with this – it comes from the value you supplied in your Python file’s details dictionary for configcomponent.

Pulling it all together, here’s the JavaScript portion of Arches’ date DataType.

define(['knockout'], function (ko) {
    var name = 'date-datatype-config';
    ko.components.register(name, {
        viewModel: function(params) {
            var self = this;
            this.search = params.search;
            if (this.search) {
                var filter = params.filterValue();
                this.viewMode = 'days';
                this.op = ko.observable(filter.op || '');
                this.searchValue = ko.observable(filter.val || '');
                this.filterValue = ko.computed(function () {
                    return {
                        op: self.op(),
                        val: self.searchValue()
                    }
                }).extend({ throttle: 750 });
                params.filterValue(this.filterValue());
                this.filterValue.subscribe(function (val) {
                    params.filterValue(val);
                });
            }
        },
        template: { require: 'text!datatype-config-templates/date' }
    });
    return name;
});

Advanced Search Rendering

If you’re supporting Advanced Search functionality for Nodes with your DataType, your Django template will include a search block, conditionally rendered by Knockout.js if the search view is active. Here’s the one from the boolean datatype:

<!-- ko if: $data.search -->
{% block search %}
<div class="col-sm-12">
    <select class="resources" data-bind="value: searchValue, chosen: {width: '100%', disable_search_threshold: 15}, options: [{id:'t', name:trueLabel}, {id:'f', name:falseLabel}], optionsText: 'name', optionsValue: 'id'">

    </select>
</div>
{% endblock search %}
<!-- /ko -->

Note the <!-- ko if: $data.search --> directive opening and closing the search block. This is not an HTML comment – it’s Knockout.js-flavored markup for the conditional rendering.

Arches’ built-in date DataType does not use the Django template block directive, but only implements advanced search, and contains a more sophisticated example of the component logic needed:

{% load i18n %}
<!-- ko if: $data.search -->
<div class="col-md-4 col-lg-3">
    <select class="resources" tabindex="-1" style="display: none;" data-bind="value: op, chosen: {width: '100%', disable_search_threshold: 15}">
        <option value="eq"> = </option>
        <option value="gt"> > </option>
        <option value="lt"> < </option>
        <option value="gte"> >= </option>
        <option value="lte"> <= </option>
    </select>
</div>

<div class="col-md-8 col-lg-9">
    <input type="" placeholder="{% trans "Date" %}" class="form-control input-md widget-input" data-bind="value: searchValue, datepicker: {format: 'YYYY-MM-DD', viewMode: viewMode, minDate: false, maxDate: false}">
</div>
<!-- /ko -->

Node-specific Configuration

This section of your template should be enclosed in Knockout-flavored markup something like: <!-- ko if: $data.graph -->, and in your Knockout function you should follow the convention and end up with something like if (this.graph) {

Here, you put form elements corresponding to any configuration you’ve implemented in your DataType. These should correspond to keys in your DataType’s defaultconfig.

Arches’ boolean DataType has the following defaultconfig:

{'falseLabel': 'No', 'trueLabel': 'Yes'}

You can see the corresponding data bindings in the Django template:

<!-- ko if: $data.graph -->
<div class="control-label">
    {% trans "Label 'True'" %}
</div>
<div class="col-xs-12 pad-no crud-widget-container">
    <input type="" id="" class="form-control input-md widget-input" data-bind="value: trueLabel, valueUpdate: 'keyup'">
</div>
<div class="control-label">
    {% trans "Label 'False'" %}
</div>
<div class="col-xs-12 pad-no crud-widget-container">
    <input type="" id="" class="form-control input-md widget-input" data-bind="value: falseLabel, valueUpdate: 'keyup'">
</div>
<!-- /ko -->

And finally, here is the boolean DataType’s JavaScript file in its entirety:

define(['knockout'], function (ko) {
    var name = 'boolean-datatype-config';
    ko.components.register(name, {
        viewModel: function(params) {
            var self = this;
            this.search = params.search;
            this.graph = params.graph;

            this.trueLabel = params.config ? params.config.trueLabel : params.node.config.trueLabel;
            this.falseLabel = params.config ? params.config.falseLabel : params.node.config.falseLabel;

            if (this.search) {
                var filter = params.filterValue();
                this.searchValue = ko.observable(filter.val || '');
                this.filterValue = ko.computed(function () {
                    return {
                        val: self.searchValue()
                    }
                });
                params.filterValue(this.filterValue());
                this.filterValue.subscribe(function (val) {
                    params.filterValue(val);
                });
            }
        },
        template: { require: 'text!datatype-config-templates/boolean' }
    });
    return name;
});

Registering your DataType

These commands are identical to working with Widgets, but you use the word datatype instead. Please refer to Command Line Reference - Widget Commands.

Card Components

Beginning in Arches 4.3, Cards are rendered using Card Components, allowing them to be composed and nested arbitrarily in various contexts within the Arches UI. Arches comes with a default Card Component that should suit most needs, but you can also create and register custom Card Components to extend the front-end behavior of Arches.

Developing Card Components is very similar to developing Widgets. A Card Component consists of a Django template and Knockout.js JavaScript file. To register your component, you’ll also need a JSON file specifying its initial configuration.

To develop your new card, you’ll place files like so in your project:

project_name/templates/views/components/cards/my-new-card.htm project_name/media/js/views/components/cards/my-new-card.js

To register and configure the Component, you’ll need a JSON configuration file:

project_name/cards/my-new-card.json

Creating a Card Component

The default template and Knockout files illustrate everything a Card Component needs, and you’ll be extending this functionality. Your template will provide conditional markup for various contexts (‘editor-tree’, ‘designer-tree’, ‘permissions-tree’, ‘form’, and ‘report’), render all the card’s Widgets, and display other information.

Here’s the template for the default Card Component:

{% load i18n %}
<!-- ko foreach: { data: [$data], as: 'self' } -->

<!-- ko if: state === 'editor-tree' -->
<li role="treeitem card-treeitem" class="jstree-node" data-bind="css: {'jstree-open': (card.tiles().length > 0 && card.expanded()), 'jstree-closed' : (card.tiles().length > 0 && !card.expanded()), 'jstree-leaf': card.tiles().length === 0}, scrollTo: card.scrollTo, container: '.resource-editor-tree'">
    <i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: function(){card.expanded(!card.expanded())}"></i>
    <a class="jstree-anchor" href="#" tabindex="-1" data-bind="css:{'filtered': card.highlight(), 'jstree-clicked': card.selected, 'child-selected': card.isChildSelected()}, click: function () { card.canAdd() ? card.selected(true) : card.tiles()[0].selected(true) },">
        <i class="fa fa-file-o" role="presentation" data-bind="css:{'filtered': card.highlight(), 'has-provisional-edits fa-file': card.doesChildHaveProvisionalEdits()}"></i>
        <span style="padding-right: 5px;" data-bind="text: card.model.name"></span>
        <!-- ko if: card.canAdd() -->
        <i class="fa fa-plus-circle add-new-tile" role="presentation" data-bind="css:{'jstree-clicked': card.selected}" data-toggle="tooltip" data-original-title="{% trans "Add New" %}"></i>
        <!-- /ko -->
    </a>
    <ul class="jstree-children" aria-expanded="true">
        <div data-bind="sortable: {
            data: card.tiles,
            beforeMove: self.beforeMove,
            afterMove: card.reorderTiles
        }">
            <li role="treeitem" class="jstree-node" data-bind="css: {'jstree-open': (cards.length > 0 && expanded), 'jstree-closed' : (cards.length > 0 && !expanded()), 'jstree-leaf': cards.length === 0}">
                <i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: function(){expanded(!expanded())}"></i>
                <a class="jstree-anchor" href="#" tabindex="-1" data-bind="click: function () { self.form.selection($data) }, css:{'jstree-clicked': selected, 'child-selected': isChildSelected(), 'filtered-leaf': card.highlight()}">
                    <i class="fa fa-file" role="presentation" data-bind="css:{'has-provisional-edits': doesChildHaveProvisionalEdits() || $data.hasprovisionaledits()}"></i>
                    <strong style="margin-right: 10px;">
                        <!-- ko if: card.widgets().length > 0 -->
                        <span data-bind="text: card.widgets()[0].label || card.model.name"></span>:
                        <div style="display: inline;" data-bind="component: {
                            name: self.form.widgetLookup[card.widgets()[0].widget_id()].name,
                            params: {
                                tile: $data,
                                node: self.form.nodeLookup[card.widgets()[0].node_id()],
                                config: self.form.widgetLookup[card.widgets()[0].widget_id()].config,
                                label: self.form.widgetLookup[card.widgets()[0].widget_id()].label,
                                value: $data.data[card.widgets()[0].node_id()],
                                type: 'resource-editor',
                                state: 'display_value'
                            }
                        }"></div>
                        <!-- /ko -->
                        <!-- ko if: card.widgets().length === 0 -->
                        <span data-bind="text: card.model.name"></span>
                        <!-- /ko -->
                    </strong>
                </a>
                <!-- ko if: cards.length > 0 -->
                <ul class="jstree-children" aria-expanded="true" data-bind="foreach: {
                        data: cards,
                        as: 'card'
                    }">
                    <!-- ko component: {
                        name: self.form.cardComponentLookup[card.model.component_id()].componentname,
                        params: {
                            state: 'editor-tree',
                            card: card,
                            tile: null,
                            loading: self.loading,
                            form: self.form
                        }
                    } --> <!-- /ko -->
                </ul>
                <!-- /ko -->
            </li>
        </div>
        <!-- /ko -->
    </ul>
</li>
<!-- /ko -->

<!-- ko if: state === 'designer-tree' -->
<li role="treeitem card-treeitem" class="jstree-node" data-bind="css: {'jstree-open': ((card.cards().length > 0 || card.widgets().length > 0) && card.expanded()), 'jstree-closed' : ((card.cards().length > 0 || card.widgets().length > 0) && !card.expanded()), 'jstree-leaf': card.cards().length === 0 && card.widgets().length === 0}, scrollTo: card.scrollTo, container: '.designer-card-tree'">
    <i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: function(){card.expanded(!card.expanded())}"></i>
    <a class="jstree-anchor" href="#" tabindex="-1" data-bind="css:{'filtered': card.highlight(), 'jstree-clicked': card.selected, 'child-selected': card.isChildSelected()}, click: function () { card.selected(true) },">
        <i class="fa fa-file-o" role="presentation"></i>
        <span style="padding-right: 5px;" data-bind="text: card.model.name"></span>
    </a>
    <!-- ko if: card.cards().length > 0 || card.widgets().length > 0 -->
    <ul class="jstree-children card-designer-tree" aria-expanded="true">
        <div data-bind="sortable: {
                data: card.widgets,
                as: 'widget',
                beforeMove: self.beforeMove,
                afterMove: function() { card.model.save() }
            }">
            <li role="treeitem" class="jstree-node jstree-leaf" data-bind="css: {
                    'jstree-last': $index() === (card.widgets().length - 1) && card.cards().length === 0
                }">
                <i class="jstree-icon jstree-ocl" role="presentation"></i>
                <a class="jstree-anchor" href="#" tabindex="-1" data-bind="click: function() { widget.selected(true) }, css:{'jstree-clicked': widget.selected, 'hover': widget.hovered}, event: { mouseover: function(){ widget.hovered(true) }, mouseout: function(){ widget.hovered(null) } }">
                    <i data-bind="css: widget.datatype.iconclass" role="presentation"></i>
                    <strong style="margin-right: 10px;" >
                        <span data-bind="text: !!(widget.label()) ? widget.label() : widget.node.name"></span>
                    </strong>
                </a>
            </li>
        </div>
        <div data-bind="sortable: {
                data: card.cards,
                as: 'childCard',
                beforeMove: self.beforeMove,
                afterMove: function() {
                    card.reorderCards();
                }
            }">
            <div data-bind="css: {
                    'jstree-last': ($index() === (card.cards().length - 1))
                }">
                <!-- ko component: {
                        name: self.form.cardComponentLookup[childCard.model.component_id()].componentname,
                        params: {
                        state: 'designer-tree',
                        card: childCard,
                        tile: null,
                        loading: self.loading,
                        form: self.form
                    }
                } --> <!-- /ko -->
            </div>
        </div>
    </ul>
    <!-- /ko -->
</li>
<!-- /ko -->


<!-- ko if: state === 'permissions-tree' -->
<li role="treeitem card-treeitem" class="jstree-node" data-bind="css: {'jstree-open': ((card.cards().length > 0 || card.widgets().length > 0) && card.expanded()), 'jstree-closed' : ((card.cards().length > 0 || card.widgets().length > 0) && !card.expanded()), 'jstree-leaf': card.cards().length === 0 && card.widgets().length === 0}">
    <i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: function(){card.expanded(!card.expanded())}"></i>
    <a class="jstree-anchor permissions-card" href="#" tabindex="-1" data-bind="css:{'jstree-clicked': card.selected, 'child-selected': card.isChildSelected()}, click: function () { card.selected(true) },">
        <i class="fa fa-file-o" role="presentation"></i>
        <span style="padding-right: 5px;" data-bind="text: card.model.name, css:{'filtered': card.highlight()}">
        </span>
        <span class="node-permissions">
            <!--ko if: card.perms -->
            <!-- ko foreach: card.perms() -->
            <i class="node-permission-icon" data-bind="css: $data.icon"></i>
            <!-- /ko -->
            <!-- /ko -->
        </span>
    </a>
    <!-- ko if: card.cards().length > 0 || card.widgets().length > 0 -->
    <ul class="jstree-children" aria-expanded="true">
        <div data-bind="sortable: {
                data: card.widgets,
                as: 'widget',
                beforeMove: self.beforeMove,
                afterMove: function() { card.model.save() }
            }">
            <li role="treeitem" class="jstree-node jstree-leaf" data-bind="css: {
                    'jstree-last': $index() === (card.widgets().length - 1) && card.cards().length === 0
                }">
                <i class="jstree-icon jstree-ocl" role="presentation"></i>
                <a class="jstree-anchor permissions-widget" href="#" tabindex="-1">
                    <i class="fa fa-file" role="presentation" ></i>
                    <strong style="margin-right: 10px;" >
                        <span data-bind="text: !!(widget.label()) ? widget.label() : widget.node.name"></span>
                    </strong>
                </a>
            </li>
        </div>
        <div data-bind="foreach: {
                data: card.cards,
                as: 'card'
            }">
            <!-- ko component: {
                name: self.form.cardComponentLookup[card.model.component_id()].componentname,
                params: {
                    state: 'permissions-tree',
                    card: card,
                    tile: null,
                    loading: self.loading,
                    form: self.form,
                    multiselect: true
                }
            } --> <!-- /ko -->
        </div>
    </ul>
    <!-- /ko -->
</li>
<!-- /ko -->


<!-- ko if: state === 'form' -->
<div class="card-component">

    <!--ko if: reviewer && provisionalTileViewModel.selectedProvisionalEdit() -->
    <div class="edit-message-container">
        <span>{% trans 'Currently showing edits by' %}</span>
        <span class="edit-message-container-user" data-bind="text: provisionalTileViewModel.selectedProvisionalEdit().username() + '.'"></span>
        <!--ko if: !provisionalTileViewModel.tileIsFullyProvisional() -->
        <a class="reset-authoritative" href='' data-bind="click: function(){provisionalTileViewModel.resetAuthoritative();}">{% trans 'Return to approved edits' %}</a>
        <!--/ko-->
        <!--ko if: provisionalTileViewModel.selectedProvisionalEdit().isfullyprovisional -->
        <span>{% trans ' This is a new contribution by a provisional editor.' %}</span>
        <!--/ko-->
    </div>
    <!--/ko-->

    <!--ko if: reviewer && provisionalTileViewModel.provisionaledits().length > 0 && !provisionalTileViewModel.selectedProvisionalEdit()-->
    <div class="edit-message-container approved">
        <div>{% trans 'Currently showing the most recent approved edits' %}</div>
    </div>
    <!--/ko-->



    <div class="new-provisional-edit-card-container">
        <!--ko if: reviewer && provisionalTileViewModel.provisionaledits().length > 0 -->
        <!--ko if: !provisionalTileViewModel.tileIsFullyProvisional() -->
        <div class='new-provisional-edits-list'>
            <div class='new-provisional-edits-header'>
                <div class='new-provisional-edits-title'>{% trans 'Provisional Edits' %}</div>
                <div class="btn btn-shim btn-danger btn-labeled btn-xs fa fa-trash new-provisional-edits-delete-all" style="padding: 3px;" data-bind="click: function(){provisionalTileViewModel.deleteAllProvisionalEdits()}">{% trans 'Delete all edits' %}</div>
            </div>
            <!--ko foreach: { data: provisionalTileViewModel.provisionaledits(), as: 'pe' } -->
            <div class='new-provisional-edit-entry' data-bind="css: {'selected': pe === $parent.provisionalTileViewModel.selectedProvisionalEdit()}, click: function(){$parent.provisionalTileViewModel.selectProvisionalEdit(pe)}">
                <div class='title'>
                    <div class='field'>
                        <span data-bind="text : pe.username"></span>
                    </div>
                    <a href='' class='field fa fa-times-circle new-delete-provisional-edit' data-bind="click : function(){$parent.provisionalTileViewModel.rejectProvisionalEdit(pe)}"></a>
                </div>
                <div class="field timestamp">
                    <span data-bind="text : pe.displaydate">@</span>
                    <span data-bind="text : pe.displaytimestamp"></span>
                </div>
            </div>
            <!-- /ko -->
        </div>
        <!--/ko-->
        <!--/ko-->


        <div class="card">

            <h4 data-bind="text: card.model.name"></h4>
            <h5 data-bind="text: card.model.instructions"></h5>

            <!-- ko if: card.widgets().length > 0 -->
            <form class="widgets" style="margin-bottom: 20px;">
                <div data-bind="foreach: {
                        data:card.widgets, as: 'widget'
                    }">
                    <div data-bind='component: {
                        name: self.form.widgetLookup[widget.widget_id()].name,
                        params: {
                            formData: self.tile.formData,
                            tile: self.tile,
                            form: self.form,
                            config: widget.configJSON,
                            label: widget.label(),
                            value: self.tile.data[widget.node_id()],
                            node: self.form.nodeLookup[widget.node_id()],
                            expanded: self.expanded,
                            graph: self.form.graph,
                            type: "resource-editor"
                        }
                    }, css:{ "active": widget.selected, "hover": widget.hovered, "widget-preview": self.preview
                }, click: function(data, e) { if (!widget.selected() && self.preview) {widget.selected(true);}
            }, event: { mouseover: function(){ if (self.preview){widget.hovered(true) } }, mouseout: function(){ if (self.preview){widget.hovered(null)} } }'></div>
                </div>
            </form>
            <!-- /ko -->
            <!-- ko if: card.widgets().length === 0 -->
            <ul class="card-summary-section" data-bind="css: {disabled: !tile.tileid}">
                <!-- ko foreach: { data: tile.cards, as: 'card' } -->
                <li class="card-summary">
                    <a href="javascript:void(0)" data-bind="click: function () {
                        if (card.parent.tileid) {
                            card.canAdd() ? card.selected(true) : card.tiles()[0].selected(true);
                        }
                    }">
                        <h4 class="card-summary-name">
                            <span data-bind="text: card.model.name"></span>
                            <!-- ko if: card.canAdd() && card.parent.tileid -->
                            <i class="fa fa-plus-circle card-summary-add"></i>
                            <!-- /ko -->
                        </h4>
                    </a>
                    <ul class="tile-summary-item" data-bind="foreach: {
                            data: card.tiles,
                            as: 'tile'
                        }">
                        <li class="tile-summary">
                            <a href="#" data-bind="click: function () { tile.selected(true) }">
                                <!-- ko if: card.widgets().length > 0 -->
                                <span data-bind="text: card.widgets()[0].label || card.model.name" class="tile-summary-label"></span>:
                                <div style="display: inline;" data-bind="component: {
                                    name: self.form.widgetLookup[card.widgets()[0].widget_id()].name,
                                    params: {
                                        tile: tile,
                                        node: self.form.nodeLookup[card.widgets()[0].node_id()],
                                        config: self.form.widgetLookup[card.widgets()[0].widget_id()].config,
                                        label: self.form.widgetLookup[card.widgets()[0].widget_id()].label,
                                        value: tile.data[card.widgets()[0].node_id()],
                                        type: 'resource-editor',
                                        state: 'display_value'
                                    }
                                }"></div>
                                <!-- /ko -->
                                <!-- ko if: card.widgets().length === 0 -->
                                <span data-bind="text: card.model.name"></span>
                                <!-- /ko -->
                            </a>
                        </li>
                    </ul>
                </li>
                <!-- /ko -->
            </ul>
            <!-- /ko -->
            <div class="install-buttons">
                <!-- ko if: tile.tileid -->
                <button class="btn btn-shim btn-warning btn-labeled btn-lg fa fa-trash" data-bind="click: function () { self.form.deleteTile(tile); }">{% trans 'Delete this record' %}</button>
                <!-- /ko -->
                <!-- ko if: tile.dirty() -->
                <!-- ko if: provisionalTileViewModel && !provisionalTileViewModel.tileIsFullyProvisional() -->
                <button class="btn btn-shim btn-danger btn-labeled btn-lg fa fa-times" data-bind="click: tile.reset">{% trans 'Cancel edit' %}</button>
                <!-- /ko -->
                    <!-- ko if: tile.tileid -->
                    <button class="btn btn-shim btn-mint btn-labeled btn-lg fa fa-plus" data-bind="click: function () { self.form.saveTile(tile); }">{% trans 'Save edit' %}</button>
                    <!-- /ko -->
                <!-- /ko -->
                <!-- ko if: !tile.tileid -->
                <button class="btn btn-shim btn-mint btn-labeled btn-lg fa fa-plus" data-bind="click: function () { self.form.saveTile(tile); }">{% trans 'Add'  %}</button>
                <!-- /ko -->
            </div>

        </div>
    </div>
</div>
<!-- /ko -->

<!-- ko if: state === 'report' -->
<div class="rp-card-section">
    <span class="rp-tile-title" data-bind="text: card.model.get('name')"></span>
    <!-- ko foreach: { data: card.tiles, as: 'tile' } -->
        <div class="rp-card-section">
            <!-- ko if: card.model.get('widgets')().length > 0 -->
                <div class="rp-report-tile" data-bind="attr: { id: tile.tileid }">
                    <dl class="dl-horizontal">
                        <!-- ko foreach: { data: card.model.get('widgets'), as: 'widget' } -->
                            <!-- ko component: {
                                name: widget.widgetLookup[widget.get("widget_id")()].name,
                                params: {
                                    config: configJSON,
                                    label: widget.get("label")(),
                                    node: widget.node,
                                    value: tile.data[widget.node.nodeid],
                                    state: "report"
                                }
                            } --><!-- /ko -->
                        <!-- /ko -->
                    </dl>
                </div>
            <!-- /ko -->

            <div class="rp-report-container-tile" data-bind="visible: card.cards().length > 0">
                <!-- ko foreach: { data: tile.cards, as: 'card' } -->
                        <!-- ko component: {
                            name: card.model.cardComponentLookup[card.model.component_id()].componentname,
                            params: {
                                state: 'report',
                                card: card
                            }
                        } --> <!-- /ko -->
                <!-- /ko -->
            </div>
        </div>
    <!-- /ko -->

    <!-- ko if: card.tiles().length === 0 -->
    <div class="row rp-report-tile rp-no-data">
        <!-- ko ifnot: card.model.get('cardid') -->
        {% trans "Sorry, you don't have access to this information" %}
        <!-- /ko -->
        <!-- ko if: card.model.get('cardid') -->
        {% trans "No data added yet for" %} "<span data-bind="text: card.model.get('name')"></span>"
        <!-- /ko -->
    </div>
    <!-- /ko -->
</div>
<!-- /ko -->

<!-- /ko -->

And here’s the Knockout file:

define(['knockout', 'bindings/scrollTo'], function(ko) {
    var viewModel = function(params) {
        this.state = params.state || 'form';
        this.preview = params.preview;
        this.loading = params.loading || ko.observable(false);
        this.card = params.card;
        this.tile = params.tile;
        if (this.preview) {
            if (!this.card.newTile) {
                this.card.newTile = this.card.getNewTile();
            }
            this.tile = this.card.newTile;
        }
        this.form = params.form;
        this.provisionalTileViewModel = params.provisionalTileViewModel;
        this.reviewer = params.reviewer;
        this.expanded = ko.observable(true);
        this.beforeMove = function(e) {
            e.cancelDrop = (e.sourceParent!==e.targetParent);
        };
    };
    return ko.components.register('default-card', {
        viewModel: viewModel,
        template: {
            require: 'text!templates/views/components/cards/default.htm'
        }
    });
});

Registering your Card Component

To register your Component, you’ll need a JSON configuration file looking a lot like this sample:

{
  "name": "My New Card",
  "componentid": "eea17d6c-0c32-4536-8a01-392df734de1c",
  "component": "/views/components/cards/my-new-card",
  "componentname": "my-new-card",
  "description": "An awesome new card that does wonderful things.",
  "defaultconfig": {}
}
componentid:Optional A UUID4 for your Component. Feel free to generate one in advance if that fits your workflow; if not, Arches will generate one for you and print it to STDOUT when you register the Component.
name:Required The name of your new Card Component, visible in the drop-down list of card components in the Arches Designer.
description:Required A brief description of your component.
component:Required The path to the component view you have developed. Example: views/components/cards/sample-datatype
componentname:Required Set this to the last part of component above.
defaultconfig:Required You can provide user-defined default configuration here. Make it a JSON dictionary of keys and values. An empty dictionary is acceptable.

Card Commands

To register your Card Component, use this command:

python manage.py card_component register --source /Documents/projects/mynewproject/mynewproject/cards/new-card-component.json

The command will confirm your Component has been registered, and you can also see it with:

python manage.py card_component list

If you make an update to your Card Component, you can load the changes to Arches with:

python manage.py card_component update --source /Documents/projects/mynewproject/mynewproject/cards/new-card-component.json

All the Card Component commands are detailed in Command Line Reference - Card Component Commands.

Plugins

Plugins allow a developer to create an independent page in Arches that is accessible from the main navigation menu. For example, you may need a customized way of visulize your resource data. A plugin would enable you to design such an interface. Plugins, like widgets and card components rely only on front-end code. Ajax queries, generally calls to the API, must be used to access any server side data.

Registering your Plugin

To register your Plugin, you’ll need a JSON configuration file looking a lot like this sample:

{
    "pluginid": "b122ede7-24a6-4fc5-a3cc-f95bfa28b1cf",
    "name": "Sample Plugin",
    "icon": "fa fa-share-alt",
    "component": "views/components/plugins/sample-plugin",
    "componentname": "sample-plugin",
    "config": {},
    "slug": "sample-plugin",
    "sortorder": 0
}
pluginid:Optional A UUID4 for your Plugin. Feel free to generate one in advance if that fits your workflow; if not, Arches will generate one for you and print it to STDOUT when you register the Plugin.
name:Required The name of your new Plugin, visible when a user hovers over the main navigation menu
icon:Required The icon visible in the main navigation menu.
component:Required The path to the component view you have developed. Example: views/components/plugins/sample-plugin
componentname:Required Set this to the last part of component above.
config:Required You can provide user-defined default configuration here. Make it a JSON dictionary of keys and values. An empty dictionary is acceptable.
slug:Required The string that will be used in the url to access your plugin
sortorder:Required The order in which your plugin will be listed if there are multiple plugins

Plugin Commands

To register your Plugin, use this command:

python manage.py plugin register --source /Documents/projects/mynewproject/mynewproject/plugins/sample-plugin.json

The command will confirm your Plugin has been registered, and you can also see it with:

python manage.py plugin list

If you make an update to your Plugin, you can load the changes to Arches with:

python manage.py plugin update --source /Documents/projects/mynewproject/mynewproject/plugins/sample-plugin.json