praxis 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/Gemfile +2 -1
  4. data/README.md +21 -27
  5. data/lib/api_browser/app/index.html +3 -3
  6. data/lib/api_browser/app/js/app.js +23 -3
  7. data/lib/api_browser/app/js/controllers/action.js +33 -21
  8. data/lib/api_browser/app/js/controllers/controller.js +3 -25
  9. data/lib/api_browser/app/js/controllers/menu.js +61 -51
  10. data/lib/api_browser/app/js/controllers/trait.js +10 -0
  11. data/lib/api_browser/app/js/controllers/type.js +8 -5
  12. data/lib/api_browser/app/js/directives/fixed_if_fits.js +9 -2
  13. data/lib/api_browser/app/js/directives/menu_item.js +59 -0
  14. data/lib/api_browser/app/js/directives/readable_list.js +87 -0
  15. data/lib/api_browser/app/js/directives/url.js +16 -0
  16. data/lib/api_browser/app/js/factories/Configuration.js +1 -2
  17. data/lib/api_browser/app/js/factories/Documentation.js +49 -7
  18. data/lib/api_browser/app/js/factories/PageInfo.js +9 -0
  19. data/lib/api_browser/app/js/factories/normalize_attributes.js +1 -2
  20. data/lib/api_browser/app/js/factories/template_for.js +9 -4
  21. data/lib/api_browser/app/sass/modules/_sidebar.scss +54 -15
  22. data/lib/api_browser/app/sass/praxis.scss +4 -0
  23. data/lib/api_browser/app/views/action.html +72 -41
  24. data/lib/api_browser/app/views/builtin/field-selector.html +24 -0
  25. data/lib/api_browser/app/views/controller.html +9 -10
  26. data/lib/api_browser/app/views/directives/menu_item.html +8 -0
  27. data/lib/api_browser/app/views/directives/url.html +3 -0
  28. data/lib/api_browser/app/views/layout.html +2 -2
  29. data/lib/api_browser/app/views/menu.html +8 -14
  30. data/lib/api_browser/app/views/navbar.html +1 -1
  31. data/lib/api_browser/app/views/trait.html +13 -0
  32. data/lib/api_browser/app/views/type/details.html +1 -1
  33. data/lib/api_browser/app/views/type.html +1 -1
  34. data/lib/api_browser/app/views/types/embedded/field-selector.html +13 -0
  35. data/lib/api_browser/app/views/types/label/primitive.html +1 -1
  36. data/lib/api_browser/app/views/types/standalone/array.html +3 -0
  37. data/lib/praxis/action_definition.rb +15 -2
  38. data/lib/praxis/collection.rb +17 -5
  39. data/lib/praxis/controller.rb +12 -3
  40. data/lib/praxis/docs/generator.rb +11 -7
  41. data/lib/praxis/extensions/field_expansion.rb +59 -0
  42. data/lib/praxis/extensions/field_selection/field_selector.rb +125 -0
  43. data/lib/praxis/extensions/field_selection.rb +10 -0
  44. data/lib/praxis/extensions/mapper_selectors.rb +16 -0
  45. data/lib/praxis/extensions/rendering.rb +43 -0
  46. data/lib/praxis/links.rb +1 -0
  47. data/lib/praxis/media_type.rb +87 -3
  48. data/lib/praxis/media_type_collection.rb +1 -1
  49. data/lib/praxis/media_type_identifier.rb +6 -1
  50. data/lib/praxis/plugins/praxis_mapper_plugin.rb +29 -10
  51. data/lib/praxis/restful_doc_generator.rb +11 -8
  52. data/lib/praxis/tasks/api_docs.rb +6 -5
  53. data/lib/praxis/types/multipart_array.rb +1 -1
  54. data/lib/praxis/version.rb +1 -1
  55. data/lib/praxis.rb +5 -0
  56. data/praxis.gemspec +4 -3
  57. data/spec/api_browser/factories/configuration_spec.js +32 -0
  58. data/spec/api_browser/factories/documentation_spec.js +75 -25
  59. data/spec/api_browser/factories/normalize_attributes_spec.js +0 -5
  60. data/spec/praxis/{types/collection_spec.rb → collection_spec.rb} +36 -23
  61. data/spec/praxis/extensions/field_expansion_spec.rb +96 -0
  62. data/spec/praxis/extensions/field_selection/field_selector_spec.rb +92 -0
  63. data/spec/praxis/extensions/rendering_spec.rb +63 -0
  64. data/spec/praxis/links_spec.rb +6 -0
  65. data/spec/praxis/media_type_collection_spec.rb +0 -1
  66. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  67. data/spec/praxis/media_type_spec.rb +101 -3
  68. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +33 -24
  69. data/spec/praxis/request_stages/request_stage_spec.rb +1 -1
  70. data/spec/praxis/types/multipart_array_spec.rb +14 -4
  71. data/spec/spec_app/app/controllers/instances.rb +6 -1
  72. data/spec/spec_app/config/environment.rb +2 -1
  73. data/spec/spec_app/design/resources/instances.rb +1 -0
  74. data/spec/spec_helper.rb +3 -1
  75. data/spec/support/spec_media_types.rb +224 -1
  76. metadata +50 -16
@@ -0,0 +1,87 @@
1
+ app.factory('Repeater', function() {
2
+ function Repeater(expression) {
3
+ var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
4
+
5
+ var lhs = match[1];
6
+ this.rhs = match[2];
7
+ this.aliasAs = match[3];
8
+ this.trackByExp = match[4];
9
+
10
+ match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);
11
+
12
+ this.valueIdentifier = match[3] || match[1];
13
+ this.keyIdentifier = match[2];
14
+ }
15
+
16
+ Repeater.prototype.$watch = function(fn) {
17
+ this.$scope.$watchCollection(this.rhs, fn);
18
+ };
19
+
20
+ Repeater.prototype.$transclude = function(length, value, key, index, fn) {
21
+ var self = this;
22
+ self._transclude(function(clone, scope) {
23
+ scope[self.valueIdentifier] = value;
24
+ if (self.keyIdentifier) scope[self.keyIdentifier] = key;
25
+ scope.$index = index;
26
+ scope.$first = (index === 0);
27
+ scope.$last = (index === (length - 1));
28
+ scope.$middle = !(scope.$first || scope.$last);
29
+ // jshint bitwise: false
30
+ scope.$odd = !(scope.$even = (index&1) === 0);
31
+ // jshint bitwise: true
32
+ fn(clone, scope);
33
+ });
34
+ };
35
+
36
+ return {
37
+ compile: function(repeatAttr, linkFn) {
38
+ return function(element, attrs) {
39
+ var expression = attrs[repeatAttr];
40
+ var repeater = new Repeater(expression);
41
+ return function($scope, $element, $attr, ctrl, $transclude) {
42
+ repeater._transclude = $transclude;
43
+ repeater.$scope = $scope;
44
+ linkFn($scope, $element, $attr, ctrl, repeater);
45
+ };
46
+ };
47
+ }
48
+ };
49
+ });
50
+ app.directive('readableList', function(Repeater) {
51
+
52
+ return {
53
+ restrict: 'E',
54
+ transclude: true,
55
+ compile: Repeater.compile('repeat', function($scope, $element, $attr, ctrl, $repeat) {
56
+ $repeat.$watch(function(inputList) {
57
+ $element.empty();
58
+ if (inputList.length == 1) {
59
+ $repeat.$transclude(1, inputList[0], null, 0, function(clone) {
60
+ $element.append(clone);
61
+ });
62
+ } else {
63
+ var finalJoin = ' and ';
64
+
65
+ var join = ', ',
66
+ arr = inputList.slice(0),
67
+ last = arr.pop(),
68
+ beforeLast = arr.pop();
69
+
70
+ _.each(arr, function(data, index) {
71
+ $repeat.$transclude(inputList.length, data, null, index, function(clone) {
72
+ $element.append(clone);
73
+ $element.append(document.createTextNode(join));
74
+ });
75
+ });
76
+ $repeat.$transclude(inputList.length, beforeLast, null, inputList.length - 2, function(clone) {
77
+ $element.append(clone);
78
+ $element.append(document.createTextNode(finalJoin));
79
+ });
80
+ $repeat.$transclude(inputList.length, last, null, inputList.length - 1, function(clone) {
81
+ $element.append(clone);
82
+ });
83
+ }
84
+ });
85
+ })
86
+ };
87
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * This directive is responsible to render a URL for an action
3
+ */
4
+ app.directive('url', function() {
5
+ return {
6
+ restrict: 'EA',
7
+ scope: {
8
+ action: '=',
9
+ example: '@'
10
+ },
11
+ templateUrl: 'views/directives/url.html',
12
+ link: function(scope, element, attrs) {
13
+ scope.showExample = scope.example == 'example' || scope.example == 'true' || 'example' in attrs;
14
+ }
15
+ };
16
+ });
@@ -7,7 +7,6 @@ app.provider('Configuration', function() {
7
7
  this.$get = function() {
8
8
  return this;
9
9
  };
10
- }).run(function(Configuration, $rootScope, $document) {
10
+ }).run(function(Configuration, $rootScope) {
11
11
  _.extend($rootScope, _.omit(Configuration, '$get'));
12
- $document[0].title = Configuration.title;
13
12
  });
@@ -1,13 +1,55 @@
1
- app.factory('Documentation', function($http) {
1
+ /**
2
+ * This service gives access to the documentation metadata
3
+ */
4
+ app.factory('Documentation', function($http, $q) {
5
+ var versions = $q.when($http.get('api/index-new.json', { cache: true }).then(function(data) {
6
+ return $q.all(_.map(data.data.versions, function(version) {
7
+ return $http.get('api/' + version + '.json', {cache: true}).then(function(versionData) {
8
+ return [version, versionData.data];
9
+ });
10
+ })).then(_.zipObject);
11
+ }));
12
+
2
13
  return {
3
- getIndex: function() {
4
- return $http.get('api/index.json', { cache: true });
14
+ /**
15
+ * Returns an array of version strings
16
+ */
17
+ versions: function() {
18
+ return versions.then(_.keys);
5
19
  },
6
- getController: function(version, name) {
7
- return $http.get('api/' + version + '/resources/' + name + '.json', { cache: true });
20
+ /**
21
+ * Returns a list of controllers and types, useful for generating navigation
22
+ */
23
+ items: function(version) {
24
+ return versions.then(function(v) { return v[version]; });
8
25
  },
9
- getType: function(version, name) {
10
- return $http.get('api/' + version + '/types/' + name + '.json', { cache: true });
26
+ /**
27
+ * Returns description of a controller
28
+ */
29
+ controller: function(version, name) {
30
+ return this.items(version).then(function(v) {
31
+ var controller = v.resources[name];
32
+ controller.id = name;
33
+ return controller;
34
+ });
35
+ },
36
+
37
+ /**
38
+ * Returns a description of a type
39
+ */
40
+ type: function(version, name) {
41
+ return versions.then(function(v) {
42
+ return v[version].schemas[name];
43
+ });
44
+ },
45
+
46
+ /**
47
+ * Returns a description of a trait
48
+ */
49
+ trait: function(version, name) {
50
+ return versions.then(function(v) {
51
+ return v[version].traits[name];
52
+ });
11
53
  }
12
54
  };
13
55
  });
@@ -0,0 +1,9 @@
1
+ app.service('PageInfo', function($rootScope) {
2
+ this.title = null;
3
+ var self = this;
4
+ $rootScope.$watch(function() {
5
+ return self.title;
6
+ }, function() {
7
+ $rootScope.subtitle = self.title;
8
+ });
9
+ });
@@ -2,11 +2,10 @@ app.factory('normalizeAttributes', function() {
2
2
  function normalize(type, attributes, parent) {
3
3
  _.forEach(attributes, function(attribute, name) {
4
4
  var path = parent.concat([name]);
5
- var example = JSON.stringify(_.get(type.example, path), null, 2);
6
5
  if (!attribute.options) attribute.options = {};
7
- if (example) attribute.options.example = example;
8
6
  if (attribute.values != null) attribute.options.values = attribute.values;
9
7
  if (attribute.default != null) attribute.options.default = attribute.default;
8
+ if (attribute.example != null) attribute.options.example = attribute.example;
10
9
  if (attribute.type && attribute.type.attributes) {
11
10
  normalize(type, attribute.type.attributes, path);
12
11
  }
@@ -36,6 +36,8 @@ app.provider('templateFor', function() {
36
36
  switch ($family) {
37
37
  case 'hash':
38
38
  return 'views/types/standalone/struct.html';
39
+ case 'array':
40
+ return 'views/types/standalone/array.html';
39
41
  default:
40
42
  return 'views/types/standalone/default.html';
41
43
  }
@@ -48,6 +50,9 @@ app.provider('templateFor', function() {
48
50
  if ($type === 'Links') {
49
51
  return 'views/types/embedded/links.html';
50
52
  }
53
+ if ($type === 'Praxis::Extensions::FieldSelection::FieldSelector') {
54
+ return 'views/types/embedded/field-selector.html';
55
+ }
51
56
  switch ($family) {
52
57
  case 'hash':
53
58
  return 'views/types/embedded/struct.html';
@@ -60,14 +65,14 @@ app.provider('templateFor', function() {
60
65
  this.register(function labelResolver($typeDefinition, $requestedTemplate, primitives) {
61
66
  'ngInject';
62
67
  if ($requestedTemplate === 'label') {
63
- if (_.contains(primitives, $typeDefinition.name)) {
64
- return 'views/types/label/primitive.html';
65
- } else if ( $typeDefinition.member_attribute !== undefined ) {
66
- if ( _.contains(primitives, $typeDefinition.member_attribute.type.name)){
68
+ if ( $typeDefinition.member_attribute !== undefined) {
69
+ if ($typeDefinition.member_attribute.anonymous || _.contains(primitives, $typeDefinition.name)) {
67
70
  return 'views/types/label/primitive_collection.html';
68
71
  } else{
69
72
  return 'views/types/label/type_collection.html';
70
73
  }
74
+ } else if ($typeDefinition.anonymous || _.contains(primitives, $typeDefinition.name)) {
75
+ return 'views/types/label/primitive.html';
71
76
  } else if ($typeDefinition.link_to) {
72
77
  return 'views/types/label/link.html';
73
78
  }
@@ -3,9 +3,6 @@
3
3
  // ------------------------------
4
4
 
5
5
  .sidebar {
6
- @media (min-width: $screen-sm-min) {
7
- width: 160px;
8
- }
9
6
  @media (min-width: $screen-lg-min) {
10
7
  width: 250px;
11
8
  }
@@ -20,25 +17,66 @@
20
17
  }
21
18
 
22
19
  .sidebar {
20
+ .open .dropdown-menu {
21
+ margin-left: 15px;
22
+ }
23
23
  .list-group {
24
24
  margin-bottom: 0;
25
25
  margin-top: 0;
26
- }
27
- .list-group-item {
28
- border-width: 1px 0;
29
- word-break: break-word;
30
- &:first-of-type {
26
+ & > menu-item:first-of-type > .menu-item {
31
27
  border-radius: 0;
32
28
  border-top: 0 none;
33
29
  }
34
- &:last-of-type {
35
- border-bottom: 0 none;
36
- }
37
- &.child-resource {
38
- padding-left: 2.5em;
30
+ }
31
+ .action {
32
+ background: $table-bg-accent;
33
+ font-size: 11px;
34
+ font-weight: bolder;
35
+ }
36
+ .menu-item {
37
+ position: relative;
38
+ display: block;
39
+ // Place the border on the list items and negative margin up for better styling
40
+ margin-bottom: -1px;
41
+ background-color: $list-group-bg;
42
+ border: 1px solid $list-group-border;
43
+ border-width: 1px 0;
44
+ word-break: break-word;
45
+ a {
46
+ display: block;
47
+ padding: 10px 15px;
48
+ // Hover state
49
+ &:hover,
50
+ &:focus {
51
+ text-decoration: none;
52
+ background-color: $list-group-hover-bg;
53
+ }
54
+
55
+ // Active class on item itself, not parent
56
+ &.active,
57
+ &.active:hover,
58
+ &.active:focus {
59
+ z-index: 2; // Place active items above their siblings for proper border styling
60
+ color: $list-group-active-color;
61
+ background-color: $list-group-active-bg;
62
+ border-color: $list-group-active-border;
63
+ font-weight: bolder;
64
+ }
39
65
  }
40
- &.group-selected {
41
- border-left: 2px solid $link-color;
66
+ .menu-item {
67
+ a.action { padding: 10px 2.5rem; }
68
+ a.main { padding: 10px 2rem; }
69
+ a.main::before {
70
+ content: "↳"
71
+ }
72
+ .menu-item {
73
+ a.action { padding: 10px 3.5rem; }
74
+ a.main { padding: 10px 3rem; }
75
+ .menu-item {
76
+ a.action { padding: 10px 4.5rem; }
77
+ a.main { padding: 10px 4rem; }
78
+ }
79
+ }
42
80
  }
43
81
  }
44
82
  }
@@ -73,6 +111,7 @@
73
111
  a {
74
112
  border-bottom: 0;
75
113
  font-size: 12px;
114
+ padding: 10px 5px;
76
115
  &:hover {
77
116
  background-color: #fff;
78
117
  border-color: transparent;
@@ -53,3 +53,7 @@
53
53
  }
54
54
  }
55
55
  }
56
+
57
+ h4, h5 {
58
+ font-weight: 600;
59
+ }
@@ -4,72 +4,103 @@
4
4
  <div ng-if="action">
5
5
  <div class="row">
6
6
  <div class="col-lg-12">
7
- <h2>
8
- {{ action.name }}
9
- </h2>
10
- <p ng-repeat="url in action.urls">
11
- <span class="label label-default verb" ng-class="url.verb | lowercase">{{ url.verb }}</span> <b>{{ url.path }}</b>
12
- </p>
7
+ <h1>
8
+ <a ui-sref="root.controller({version: apiVersion, controller: controller.id})">{{controller.display_name}}</a> » {{ action.name }}
9
+ </h1>
10
+ <url action="action"></url>
13
11
  <p ng-bind-html="action.description | markdown"></p>
12
+ <p class="traits" ng-if="action.traits.length > 0">
13
+ This action is&nbsp;
14
+ <readable-list repeat="trait in action.traits"><a ui-sref="root.trait({version: apiVersion, trait: trait})">{{trait}}</a></readable-list>.
15
+ </p>
14
16
  </div>
15
17
  </div>
16
18
 
17
19
  <div class="row" ng-if="action.headers.type.attributes">
18
20
  <div class="col-lg-12">
19
- <h3>Request Headers</h3>
21
+ <h2>Request Headers</h2>
20
22
  <type-placeholder details="action.headers.type.attributes" type="action.headers.type" template="standalone"></type-placeholder>
21
23
  </div>
22
24
  </div>
23
25
 
24
26
  <div class="row" ng-if="action.params.type.attributes">
25
27
  <div class="col-lg-12">
26
- <h3>Request Parameters</h3>
28
+ <h2>Request Parameters</h2>
27
29
  <type-placeholder details="action.params.type.attributes" type="action.params.type" template="standalone"></type-placeholder>
28
30
  </div>
29
31
  </div>
30
32
 
31
33
  <div class="row" ng-if="action.payload.type">
32
34
  <div class="col-lg-12">
33
- <h3>Request Body</h3>
35
+ <h2>Request Body</h2>
34
36
  <type-placeholder type="action.payload.type" template="standalone" details="action.payload.type.attributes"></type-placeholder>
35
37
  </div>
36
38
  </div>
37
39
 
40
+ <div class="row">
41
+ <div class="col-lg-12">
42
+ <h2>Request Example</h2>
43
+ <div ng-if="!action.payload.examples">
44
+ <h4>URL</h4>
45
+ <url action="action" example></url>
46
+ <div ng-if="action.headers.example">
47
+ <h4>Headers</h4>
48
+ <pre><span ng-repeat="(header, value) in action.headers.example">{{header}}: {{value}}
49
+ </span></pre>
50
+ </div>
51
+ </div>
52
+ <tabset ng-if="action.payload.examples">
53
+ <tab ng-repeat="(type, example) in action.payload.examples" heading="{{type}}">
54
+ <h4>URL</h4>
55
+ <url action="action" example></url>
56
+ <div ng-if="action.headers.example">
57
+ <h4>Headers</h4>
58
+ <pre><span ng-if="example.content_type">Content-Type: {{example.content_type}}
59
+ </span><span ng-repeat="(header, value) in action.headers.example">{{header}}: {{value}}
60
+ </span></pre>
61
+ </div>
62
+ <h4>Request body</h4>
63
+ <pre>{{ example.body }}</pre>
64
+ </tab>
65
+ </tabset>
66
+ </div>
67
+ </div>
68
+
38
69
  <div class="row" ng-if="hasResponses()">
39
70
  <div class="col-lg-12">
40
- <h3>Responses</h3>
41
- <div class="table-responsive">
42
- <table class="table table-striped table-bordered">
43
- <thead>
44
- <tr>
45
- <th>Code</th>
46
- <th>Name</th>
47
- <th>Media Type</th>
48
- <th>Description</th>
49
- </tr>
50
- </thead>
51
- <tbody>
52
- <tr ng-repeat="response in responses">
53
- <td>
54
- <span ng-if="response.isMultipart">
55
- <em>Parts Like:</em>
56
- </span>
57
- <span>{{response.status}}</span>
58
- </td>
59
- <td>
60
- {{response.name}}
61
- </td>
62
- <td>
63
- {{response.media_type.id || response.media_type.identifier}}
64
- </td>
65
- <td>
66
- <attribute-description attribute="response"></attribute-description>
67
- </td>
68
- </tr>
69
- </tbody>
70
- </table>
71
+ <h2>Responses</h2>
72
+ <div class="panel panel-default" ng-repeat="response in responses">
73
+ <div class="panel-heading"><h4 class="panel-title">{{response.name}}</h4></div>
74
+ <div class="panel-body">
75
+ <div class="row">
76
+ <div class="col-md-4">
77
+ <h5>Status</h5>
78
+ <p>{{ response.status }}</p>
79
+ <div ng-if="response.examples.json">
80
+ <h5>Content-Type</h5>
81
+ <p>{{ response.examples.json.content_type }}</p>
82
+ </div>
83
+ <div ng-if="response.payload.id">
84
+ <h5>Media Type</h5>
85
+ <p><type-placeholder template="label" type="response.payload"></type-placeholder></p>
86
+ </div>
87
+ </div>
88
+ <div class="col-md-8">
89
+ <div ng-if="response.payload.examples">
90
+ <h5>Response Example</h5>
91
+ <tabset ng-if="numExamples > 1">
92
+ <tab ng-repeat="(type, example) in response.payload.examples" heading="{{type}}">
93
+ <pre>{{ example.body }}</pre>
94
+ </tab>
95
+ </tabset>
96
+ <div ng-if="response.numExamples == 1">
97
+ <pre ng-repeat="(type, example) in response.payload.examples">{{ example.body }}</pre>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
71
103
  </div>
72
104
  </div>
73
105
  </div>
74
106
  </div>
75
- <hr class="action-divider" />
@@ -0,0 +1,24 @@
1
+ <div class="row">
2
+ <div class="col-lg-12">
3
+ <h1 class="page-header">FieldSelector</h1>
4
+
5
+ <p>Field Selectors allow you to specify in a request exactly what data you
6
+ wish to recieve in the response. This allows the client to get an efficient
7
+ response with only the fields required.</p>
8
+ <p>The fields can come from the endpointʼs associated Schema, or from an
9
+ attribute exposed in any of the views of the resource. Multiple fields are
10
+ separated by commas.</p>
11
+ <p>You can also select properties from nested schemas from parent schema
12
+ using curly braces. For example <code>title,user{name,email}</code> would lead to a
13
+ response like this:</p>
14
+ <pre>{
15
+ "title": "Nice Blog Post",
16
+ "user": {
17
+ "name": "John Appleseed",
18
+ "email": "john@example.com"
19
+ }
20
+ }</pre>
21
+ <p>If you do not specify attributes of nested properties, the <code>default</code>
22
+ view will be used to render them.</p>
23
+ </div>
24
+ </div>
@@ -7,7 +7,13 @@
7
7
  <h1 class="page-header">
8
8
  {{ controller.name | resourceName }}
9
9
  </h1>
10
- <p ng-bind-html="controller.description | markdown"></p>
10
+
11
+ <p ng-bind-html="controller.description | markdown"></p>
12
+
13
+ <p class="traits" ng-if="controller.traits.length > 0">
14
+ This resource is&nbsp;
15
+ <readable-list repeat="trait in controller.traits"><a ui-sref="root.trait({version: apiVersion, trait: trait})">{{trait}}</a></readable-list>.
16
+ </p>
11
17
  </div>
12
18
  </div>
13
19
  <div class="row" ng-if="controller.actions.length">
@@ -25,13 +31,10 @@
25
31
  <tbody>
26
32
  <tr ng-repeat="action in controller.actions">
27
33
  <td>
28
- <a ui-sref="root.controller.action({action: action.name, controller: controllerName, version: apiVersion})">{{ action.name }}</a>
34
+ <a ui-sref="root.action({action: action.name, controller: controllerName, version: apiVersion})">{{ action.name }}</a>
29
35
  </td>
30
36
  <td>
31
- <div ng-repeat="url in action.urls">
32
- <div class="label label-default verb" ng-class="url.verb | lowercase">{{ url.verb }}</div>
33
- <strong>{{ url.path }}</strong>
34
- </div>
37
+ <url action="action"></url>
35
38
  </td>
36
39
  <td style="font-size: 110%;">
37
40
  <p ng-bind-html="action.description | markdown"></p>
@@ -49,8 +52,4 @@
49
52
  <h2>Schema</h2>
50
53
  <p ><b>Internet Media-type: {{ controller.media_type.identifier }}</b></p>
51
54
  </div>
52
- <h1>Actions</h1>
53
- <div ng-controller="ActionCtrl" ng-repeat="action in controller.actions">
54
- <div id="action-{{action.name}}" ng-include="'views/action.html'"></div>
55
- </div>
56
55
  </div>
@@ -0,0 +1,8 @@
1
+ <div class="menu-item" ng-if="shouldShow">
2
+ <a href="{{link.stateRef}}"
3
+ ng-class="{active: isActive, 'child-resource': link.parent, 'action': link.isAction, 'main': !link.isAction}">
4
+ {{ link.name }}
5
+ </a>
6
+ <menu-item ng-repeat="action in link.actions" link="action"></menu-item>
7
+ <menu-item ng-repeat="resource in link.childResources" link="resource"></menu-item>
8
+ </div>
@@ -0,0 +1,3 @@
1
+ <p ng-repeat="url in action.urls">
2
+ <span class="label label-default verb" ng-class="url.verb | lowercase">{{ url.verb }}</span>&nbsp;<b>{{ showExample ? url.example : url.path }}</b>
3
+ </p>
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div class="container" ng-cloak>
4
4
  <div class="row">
5
- <div class="col-sm-3" ng-controller="MenuCtrl" ng-include="'views/menu.html'"></div>
6
- <div class="col-sm-9" ui-view=""></div>
5
+ <div class="col-sm-4 col-md-3" ng-controller="MenuCtrl" ng-include="'views/menu.html'"></div>
6
+ <div class="col-sm-8 col-md-9" ui-view="" autoscroll="true"></div>
7
7
  </div>
8
8
  </div>
@@ -6,7 +6,7 @@
6
6
  <button type="button" class="btn btn-success" dropdown-toggle ng-disabled="disabled">
7
7
  {{:: versionLabel}}: {{selectedVersion}} <span class="caret"></span>
8
8
  </button>
9
- <ul class="dropdown-menu col-sm-12" role="menu">
9
+ <ul class="dropdown-menu" role="menu">
10
10
  <li ng-repeat="version in versions">
11
11
  <a ng-click="select(version)" dropdown-toggle>{{version}}</a>
12
12
  </li>
@@ -23,23 +23,17 @@
23
23
  <tabset justified="true" class="tab-list-group">
24
24
  <tab heading="Resources" active="active.resources">
25
25
  <div class="list-group">
26
- <a ng-repeat="link in availableResources()"
27
- href="{{link.stateRef}}"
28
- class="list-group-item"
29
- ng-hide="expandChildren && link.parent && selectedGrandfatherType != link.grandfather"
30
- ng-class="{active: link.typeName == currentType, 'child-resource': link.parent, 'group-selected': selectedGrandfatherType == link.grandfather}">
31
- {{ link.name }}
32
- </a>
26
+ <menu-item ng-repeat="link in availableResources() | orderBy: 'name'" link="link" toplevel="true"></menu-item>
33
27
  </div>
34
28
  </tab>
35
29
  <tab heading="Schemas" active="active.schemas">
36
30
  <div class="list-group">
37
- <a ng-repeat="link in availableOthers()"
38
- href="{{link.stateRef}}"
39
- class="list-group-item"
40
- ng-class="{active: link.typeName == currentType}">
41
- {{ link.name }}
42
- </a>
31
+ <menu-item ng-repeat="link in availableSchemas() | orderBy: 'name'" link="link" toplevel="true"></menu-item>
32
+ </div>
33
+ </tab>
34
+ <tab heading="Traits" active="active.traits" ng-if="availableTraits().length > 0">
35
+ <div class="list-group">
36
+ <menu-item ng-repeat="link in availableTraits() | orderBy: 'name'" link="link" toplevel="true"></menu-item>
43
37
  </div>
44
38
  </tab>
45
39
  </tabset>
@@ -2,7 +2,7 @@
2
2
  <div class="navbar navbar-default navbar-fixed-top" role="navigation">
3
3
  <div class="container">
4
4
  <div class="navbar-header">
5
- <a class="navbar-brand" href="#">{{:: title}}</a>
5
+ <a class="navbar-brand" href="#/">{{:: title}}</a>
6
6
  </div>
7
7
  </div>
8
8
  </div>
@@ -0,0 +1,13 @@
1
+ <div ng-if="error" class="alert alert-danger">
2
+ <p>The requested trait could not be found.</p>
3
+ </div>
4
+ <div ng-if="trait">
5
+ <div class="row">
6
+ <div class="col-lg-12">
7
+ <h1>
8
+ {{ traitName | resourceName }}
9
+ </h1>
10
+ </div>
11
+ </div>
12
+ <div ng-if="trait.description" ng-bind-html="trait.description | markdown"></div>
13
+ </div>