praxis 0.18.1 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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>