diff --git a/app/components/mixin-property.js b/app/components/mixin-property.js
index 27954c2dc1..92af19ceb3 100644
--- a/app/components/mixin-property.js
+++ b/app/components/mixin-property.js
@@ -1,6 +1,6 @@
import { computed } from '@ember/object';
import Component from '@ember/component';
-import { equal, alias } from '@ember/object/computed';
+import { equal, alias, and } from '@ember/object/computed';
export default Component.extend({
isEdit: false,
@@ -17,19 +17,31 @@ export default Component.extend({
txtValue: null,
dateValue: null,
- isCalculated: computed('model.value.type', function() {
- return this.get('model.value.type') !== 'type-descriptor';
+ isCalculated: computed('valueType', function() {
+ return this.get('valueType') !== 'type-descriptor';
}),
- isEmberObject: equal('model.value.type', 'type-ember-object'),
+ valueType: alias('model.value.type'),
+
+ isService: alias('model.isService'),
+
+ isOverridden: alias('model.overridden'),
+
+ isEmberObject: equal('valueType', 'type-ember-object'),
isComputedProperty: alias('model.value.computed'),
- isFunction: equal('model.value.type', 'type-function'),
+ isFunction: equal('valueType', 'type-function'),
+
+ isArray: equal('valueType', 'type-array'),
- isArray: equal('model.value.type', 'type-array'),
+ isDate: equal('valueType', 'type-date'),
- isDate: equal('model.value.type', 'type-date'),
+ isDepsExpanded: false,
+
+ hasDependentKeys: and('model.dependentKeys.length', 'isCalculated'),
+
+ showDependentKeys: and('isDepsExpanded', 'hasDependentKeys'),
_parseTextValue(value) {
let parsedValue;
@@ -48,6 +60,9 @@ export default Component.extend({
},
actions: {
+ toggleDeps() {
+ this.toggleProperty('isDepsExpanded');
+ },
valueClick() {
if (this.get('isEmberObject') || this.get('isArray')) {
this.get('mixin').send('digDeeper', this.get('model'));
@@ -64,7 +79,7 @@ export default Component.extend({
}
let value = this.get('model.value.inspect');
- let type = this.get('model.value.type');
+ let type = this.get('valueType');
if (type === 'type-string') {
value = `"${value}"`;
}
diff --git a/app/styles/base.scss b/app/styles/base.scss
index 8178774d4d..5deb108ecc 100644
--- a/app/styles/base.scss
+++ b/app/styles/base.scss
@@ -1,3 +1,9 @@
+:root {
+ --font-sans-serif: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Cantarell', 'Oxygen', 'Ubuntu', 'Helvetica Neue', helvetica, arial, sans-serif;
+ --font-monospace: 'SF Mono', Menlo, Monaco, monospace;
+}
+
+
h1, h2, h3, h4, h5, h6 {
margin: 0;
padding: 0;
@@ -10,7 +16,7 @@ html, body {
}
body, input {
- font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Cantarell', 'Oxygen', 'Ubuntu', 'Helvetica Neue', helvetica, arial, sans-serif;
+ font-family: var(--font-sans-serif);
color: var(--base26);
}
@@ -24,5 +30,5 @@ a {
}
code {
- font-family: Menlo, Monaco, monospace;
+ font-family: var(--font-monospace);
}
diff --git a/app/styles/colors.scss b/app/styles/colors.scss
index 87dfc99008..a85b55e452 100644
--- a/app/styles/colors.scss
+++ b/app/styles/colors.scss
@@ -48,6 +48,10 @@
--spec12: #7c027c;
--spec13: #4977d2;
--spec14: #ecf1fa;
+ --spec15: #cd61a7;
+ --spec16: #7fb56d;
+ --spec17: #328509;
+ --spec18: #9c0f9c;
}
.theme--dark {
diff --git a/app/styles/mixin.scss b/app/styles/mixin.scss
index 8983a060bd..32b4c602c0 100644
--- a/app/styles/mixin.scss
+++ b/app/styles/mixin.scss
@@ -39,15 +39,17 @@ $mixin-left-padding: 22px;
.mixin__name:before {
position: absolute;
top: 5px;
- left: 7px;
+ left: 6px;
content: "\25B6";
color: var(--base18);
margin-right: 5px;
font-size: 90%;
+ transform: rotate(0deg);
+ transition: all 0.1s;
}
.mixin_state_expanded .mixin__name:before {
- content: "\25BC";
+ transform: rotate(90deg);
}
.mixin__properties {
@@ -56,7 +58,7 @@ $mixin-left-padding: 22px;
margin: 0;
padding: 3px 0 5px 0;
font-size: 12px;
- font-family: 'SF Mono', Menlo, Monaco, monospace;
+ font-family: var(--font-monospace);
}
.mixin__property {
@@ -68,6 +70,25 @@ $mixin-left-padding: 22px;
flex-direction: row;
align-items: center;
min-height: 19px;
+ padding-top: 1px;
+ padding-bottom: 1px;
+
+ .mixin__cp-toggle {
+ width: $mixin-left-padding;
+ outline: none;
+
+ svg {
+ transition: all 0.2s;
+ transform:
+ rotate(-90deg)
+ translateY(1px);
+ }
+ }
+ .mixin__cp-toggle--expanded {
+ svg {
+ transform: rotate(0deg);
+ }
+ }
}
/* Errors */
@@ -97,16 +118,9 @@ $mixin-left-padding: 22px;
}
.pad { width: $mixin-left-padding; }
- .mixin__calc-btn {
- width: $mixin-left-padding;
-
- img {
- max-height: 16px;
- }
- }
- .mixin__calc-btn--calculated {
- opacity: 0.4;
+ .mixin__calc-btn svg {
+ path, circle { fill: var(--spec18); }
}
.mixin__send-btn {
@@ -120,7 +134,7 @@ $mixin-left-padding: 22px;
}
.mixin__property-name {
- color: var(--spec04);
+ color: var(--base26);
}
.mixin__property-value {
@@ -149,7 +163,7 @@ $mixin-left-padding: 22px;
}
.mixin__property .type-object {
- color: var(--base15);
+ color: var(--base20);
}
.mixin__property .type-ember-object,
@@ -157,12 +171,8 @@ $mixin-left-padding: 22px;
cursor: pointer;
}
-.mixin__property-calculated-value {
- flex: 1;
- color: var(--base15);
-}
-
-.mixin__property .type-null, .mixin__property .type-boolean {
+.mixin__property .type-null,
+.mixin__property .type-boolean {
color: var(--spec10);
}
@@ -171,6 +181,9 @@ $mixin-left-padding: 22px;
color: var(--spec05);
}
+.mixin__property .type-service {
+ color: var(--spec17);
+}
.mixin__property_state_overridden {
diff --git a/app/styles/object_inspector.scss b/app/styles/object_inspector.scss
index 5e889bd960..ffb421a497 100644
--- a/app/styles/object_inspector.scss
+++ b/app/styles/object_inspector.scss
@@ -10,3 +10,94 @@
margin-left: 39px;
word-break: break-all;
}
+
+.mixin__property-dependency-list {
+ position: relative;
+ margin-bottom: 5px;
+ margin-top: 2px;
+ padding-top: 2px;
+
+ & > svg {
+ position: absolute;
+ top: -4px;
+ left: 28px;
+ width: 19px;
+ height: 10px;
+ }
+
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+}
+
+.mixin__property-dependency-item {
+ position: relative;
+ margin-bottom: 2px;
+ margin-left: 55px;
+ font-size: 11px;
+ color: var(--base21);
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -5px;
+ left: -9px;
+ width: 1px;
+ height: 8px;
+ background-color: #888;
+ }
+
+ svg {
+ position: absolute;
+ bottom: 2px;
+ left: -13px;
+
+ ellipse {
+ stroke: #888;
+ fill: #fff;
+ }
+ }
+}
+
+.mixin__property-dependency-item:first-child::before { display: none; }
+
+.mixin__property-icon-container {
+ width: $mixin-left-padding;
+}
+
+.mixin__property-icon {
+ display: inline-block;
+ width: 17px;
+ height: 17px;
+ border-radius: 3px;
+ color: #fff;
+ font-size: 13px;
+ line-height: 17px;
+ font-family: var(--font-monospace);
+ text-align: center;
+ user-select: none;
+ -webkit-user-select: none;
+}
+
+.mixin__property-icon--service {
+ background-color: var(--spec16);
+ &::before {
+ content: "S";
+ }
+}
+
+.mixin__property-icon--computed {
+ background-color: var(--spec15);
+ &::before {
+ content: "C";
+ }
+}
+
+.mixin__property-icon--property {
+ background-color: var(--base11);
+ &::before {
+ content: "P";
+ }
+}
\ No newline at end of file
diff --git a/app/templates/components/mixin-details.hbs b/app/templates/components/mixin-details.hbs
index 071b97619a..67e546524e 100644
--- a/app/templates/components/mixin-details.hbs
+++ b/app/templates/components/mixin-details.hbs
@@ -1,20 +1,21 @@
{{#if model.errors.length}}
-
-
- Errors
-
- Trace in the console
-
-
-
- {{#each model.errors as |error|}}
-
- Error while computing: {{error.property}}
+
+
+ Errors
+
+ Trace in the console
+
+
+
+ {{#each model.errors as |error|}}
+
+ Error while computing: {{error.property}}
+
+ {{/each}}
- {{/each}}
-
{{/if}}
+
{{#each model.mixins as |item|}}
{{#mixin-detail model=item mixinDetails=this as |mixin|}}
@@ -27,27 +28,119 @@
{{#each mixin.sortedProperties as |prop|}}
{{#mixin-property model=prop mixin=mixin as |property|}}
- -
- {{#if property.model.value.computed}}
-
+
-
+
+ {{#if property.hasDependentKeys}}
+
{{else}}
{{/if}}
- {{property.model.name}}:
+
+
+ {{#if property.isService}}
+
+
+
+ {{else if property.isComputedProperty}}
+
+
+
+ {{else}}
+
+
+
+ {{/if}}
+
+
+
+ {{#if property.isService}}
+
+ {{property.model.name}}
+
+ {{else}}
+ {{#if property.hasDependentKeys}}
+
+ {{property.model.name}}
+
+ {{else}}
+ {{property.model.name}}
+ {{/if}}
+ {{/if}}
+
+
+ :
+
+
{{#unless property.isEdit}}
- {{property.model.value.inspect}}
+ {{#if (and property.isComputedProperty (not property.isCalculated))}}
+
+ {{else}}
+
+ {{property.model.value.inspect}}
+
+ {{/if}}
{{else}}
{{#unless property.isDate}}
- {{property-field value=property.txtValue finished-editing="finishedEditing" save-property="saveProperty" propertyComponent=property
- class="mixin__property-value-txt js-object-property-value-txt"}}
+ {{property-field
+ value=property.txtValue
+ finished-editing="finishedEditing"
+ save-property="saveProperty"
+ propertyComponent=property
+ class="mixin__property-value-txt js-object-property-value-txt"}}
{{else}}
- {{date-property-field value=property.dateValue format="YYYY-MM-DD"
- class="mixin__property-value-txt js-object-property-value-date" onSelection=(action "dateSelected" target=property) cancel=(action "finishedEditing" target=property)}}
+ {{date-property-field
+ value=property.dateValue
+ format="YYYY-MM-DD"
+ class="mixin__property-value-txt js-object-property-value-date"
+ onSelection=(action "dateSelected" target=property)
+ cancel=(action "finishedEditing" target=property)}}
{{/unless}}
{{/unless}}
- (Overridden by {{property.model.overridden}})
-
-
+
+ (Overridden by {{property.model.overridden}})
+
+
+
+
+ {{#if property.showDependentKeys}}
+ -
+ {{svg-jar "dependent-key-connection" width="20px" height="10px"}}
+
+ {{#each property.model.dependentKeys as |depKey index|}}
+ -
+ {{svg-jar "dependent-key-bullet" width="9px" height="9px"}}
+
+ {{depKey}}
+
+
+ {{/each}}
+
+
+ {{/if}}
{{/mixin-property}}
{{else}}
- No Properties
diff --git a/ember_debug/object-inspector.js b/ember_debug/object-inspector.js
index c9acf9dfee..3c542d2c8a 100644
--- a/ember_debug/object-inspector.js
+++ b/ember_debug/object-inspector.js
@@ -232,7 +232,6 @@ export default EmberObject.extend(PortMixin, {
if (this.canSend(object)) {
let details = this.mixinsForObject(object);
-
this.sendMessage('updateObject', {
parentObject: objectId,
property,
@@ -342,7 +341,11 @@ export default EmberObject.extend(PortMixin, {
// for more details.
if (compareVersion(VERSION, '2.11.0') !== -1) {
if (!name && typeof mixin.toString === 'function') {
- name = mixin.toString();
+ try {
+ name = mixin.toString();
+ } catch (e) {
+ name = '(Unable to convert Object to string)';
+ }
}
}
if (!name) {
@@ -468,7 +471,25 @@ function addProperties(properties, hash) {
continue;
}
let options = { isMandatorySetter: isMandatorySetter(hash, prop) };
+
+ if (typeof hash[prop] === 'object' && hash[prop] !== null) {
+ options.isService = hash[prop].type === 'service';
+ if (!options.isService) {
+ if (hash[prop].constructor) {
+ options.isService = hash[prop].constructor.isServiceFactory;
+ }
+ }
+ }
+
if (isComputed(hash[prop])) {
+ options.dependentKeys = (hash[prop]._dependentKeys || []).map((key) => key.toString());
+ if (!options.isService) {
+ if (typeof hash[prop]._getter === 'function') {
+ options.code = Function.prototype.toString.call(hash[prop]._getter);
+ } else {
+ options.code = '';
+ }
+ }
options.readOnly = hash[prop]._readOnly;
}
replaceProperty(properties, prop, hash[prop], options);
@@ -493,6 +514,10 @@ function replaceProperty(properties, name, value, options) {
let prop = { name, value: inspectValue(value) };
prop.isMandatorySetter = options.isMandatorySetter;
prop.readOnly = options.readOnly;
+ prop.dependentKeys = options.dependentKeys || [];
+ let hasServiceFootprint = prop.value && typeof prop.value.inspect === 'string' ? prop.value.inspect.includes('@service:') : false;
+ prop.isService = options.isService || hasServiceFootprint;
+ prop.code = options.code;
properties.push(prop);
}
diff --git a/public/assets/svg/dependent-key-bullet.svg b/public/assets/svg/dependent-key-bullet.svg
new file mode 100644
index 0000000000..41cb878cc5
--- /dev/null
+++ b/public/assets/svg/dependent-key-bullet.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/assets/svg/dependent-key-connection.svg b/public/assets/svg/dependent-key-connection.svg
new file mode 100644
index 0000000000..4d4653abbe
--- /dev/null
+++ b/public/assets/svg/dependent-key-connection.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/assets/svg/disclosure-triangle.svg b/public/assets/svg/disclosure-triangle.svg
index dff1f749de..bb2f84a318 100644
--- a/public/assets/svg/disclosure-triangle.svg
+++ b/public/assets/svg/disclosure-triangle.svg
@@ -1,3 +1,3 @@
diff --git a/tests/acceptance/object-inspector-test.js b/tests/acceptance/object-inspector-test.js
index de8beb37c5..d5b6285458 100644
--- a/tests/acceptance/object-inspector-test.js
+++ b/tests/acceptance/object-inspector-test.js
@@ -114,7 +114,6 @@ module('Object Inspector', function(hooks) {
assert.equal(firstDetail.querySelectorAll('.js-object-property').length, 1);
assert.dom(firstDetail.querySelector('.js-object-property-name')).hasText('numberProperty');
assert.dom(firstDetail.querySelector('.js-object-property-value')).hasText('1');
-
await click(firstDetail.querySelector('.js-object-detail-name'));
assert.dom(firstDetail).hasNoClass('mixin_state_expanded', 'Expanded detail minimizes on click.');
@@ -213,6 +212,137 @@ module('Object Inspector', function(hooks) {
assert.dom('.js-object-property-value').hasText('Computed value');
});
+ test("Service highlight", async function(assert) {
+ await visit('/');
+
+ let obj = {
+ name: 'My Object',
+ objectId: 'myObject',
+ details: [{
+ name: 'Detail',
+ properties: [{
+ name: 'serviceProp',
+ isService: true,
+ value: {
+ inspect: '',
+ computed: true
+ }
+ }]
+ }]
+ };
+
+ await triggerPort(this, 'objectInspector:updateObject', obj);
+ await click('.js-object-detail-name');
+
+ assert.equal(findAll('.mixin__property--group').length, 1);
+ assert.equal(findAll('.mixin__property-icon--service').length, 1);
+ assert.equal(findAll('.js-property-name-service').length, 1);
+ assert.equal(findAll('.mixin__property-dependency-list').length, 0);
+ assert.equal(findAll('.mixin__property-dependency-item').length, 0);
+ assert.equal(findAll('.mixin__property-dependency-item > .mixin__property-dependency-name').length, 0);
+ });
+
+ test("Computed properties no dependency", async function (assert) {
+ await visit('/');
+
+ let obj = {
+ name: 'My Object',
+ objectId: 'myObject',
+ details: [{
+ name: 'Detail',
+ properties: [{
+ name: 'computedProp',
+ dependentKeys: [],
+ value: {
+ inspect: '',
+ type: 'type-descriptor',
+ computed: true
+ }
+ }]
+ }]
+ };
+
+ await triggerPort(this, 'objectInspector:updateObject', obj);
+ await click('.js-object-detail-name');
+ await click('.js-calculate');
+
+ assert.equal(name, 'objectInspector:calculate');
+ assert.deepEqual(message, { objectId: 'myObject', property: 'computedProp', mixinIndex: 0 });
+ await triggerPort(this, 'objectInspector:updateProperty', {
+ objectId: 'myObject',
+ property: 'computedProp',
+ value: {
+ inspect: 'Computed value',
+ computed: 'foo-bar'
+ },
+ mixinIndex: 0
+ });
+
+ assert.equal(findAll('.mixin__property--group').length, 0);
+
+ await click('.mixin__property-icon--computed');
+
+ assert.equal(findAll('.mixin__property-dependency-list').length, 0);
+ assert.equal(findAll('.mixin__property-dependency-item').length, 0);
+ assert.equal(findAll('.mixin__property-dependency-item > .mixin__property-dependency-name').length, 0);
+
+ await click('.mixin__property-icon--computed');
+
+ assert.equal(findAll('.mixin__property-dependency-list').length, 0);
+ assert.equal(findAll('.mixin__property-dependency-item').length, 0);
+ assert.equal(findAll('.mixin__property-dependency-item > .mixin__property-dependency-name').length, 0);
+ });
+
+ test("Computed properties dependency expand", async function (assert) {
+ await visit('/');
+
+ let obj = {
+ name: 'My Object',
+ objectId: 'myObject',
+ details: [{
+ name: 'Detail',
+ properties: [{
+ name: 'computedProp',
+ dependentKeys: ['foo.@each.bar'],
+ value: {
+ inspect: '',
+ type: 'type-descriptor',
+ computed: true
+ }
+ }]
+ }]
+ };
+ await triggerPort(this, 'objectInspector:updateObject', obj);
+ await click('.js-object-detail-name');
+ await click('.js-calculate');
+
+ assert.equal(name, 'objectInspector:calculate');
+ assert.deepEqual(message, { objectId: 'myObject', property: 'computedProp', mixinIndex: 0 });
+ await triggerPort(this, 'objectInspector:updateProperty', {
+ objectId: 'myObject',
+ property: 'computedProp',
+ value: {
+ inspect: 'Computed value',
+ computed: 'foo-bar'
+ },
+ mixinIndex: 0
+ });
+
+ assert.equal(findAll('.mixin__property--group').length, 1);
+
+ await click('.mixin__property-icon--computed');
+
+ assert.equal(findAll('.mixin__property-dependency-list').length, 1);
+ assert.equal(findAll('.mixin__property-dependency-item').length, 1);
+ assert.equal(findAll('.mixin__property-dependency-item > .mixin__property-dependency-name').length, 1);
+
+ await click('.mixin__property-icon--computed');
+
+ assert.equal(findAll('.mixin__property-dependency-list').length, 0);
+ assert.equal(findAll('.mixin__property-dependency-item').length, 0);
+ assert.equal(findAll('.mixin__property-dependency-item > .mixin__property-dependency-name').length, 0);
+ });
+
test("Properties are bound to the application properties", async function (assert) {
await visit('/');
diff --git a/tests/ember_debug/object-inspector-test.js b/tests/ember_debug/object-inspector-test.js
index b586011b51..f39ab4f886 100644
--- a/tests/ember_debug/object-inspector-test.js
+++ b/tests/ember_debug/object-inspector-test.js
@@ -4,6 +4,7 @@ import Component from '@ember/component';
import { run } from '@ember/runloop';
import { guidFor } from '@ember/object/internals';
import EmberObject, { computed } from '@ember/object';
+import Service from '@ember/service';
import Ember from 'ember';
import { module, test } from 'qunit';
import { settings as nativeDomHelpersSettings } from 'ember-native-dom-helpers';
@@ -368,6 +369,44 @@ module('Ember Debug - Object Inspector', function(hooks) {
assert.ok(message.details[4].name !== 'MixinToSkip', 'Correctly skips properties');
});
+
+ test("Service should be successfully tagged as service on serialization", function(assert) {
+ let inspectedService = Service.extend({
+ fooBoo() {
+ return true;
+ }
+ }).create();
+
+ let inspected = EmberObject.extend({
+ service: inspectedService
+ }).create();
+
+ objectInspector.sendObject(inspected);
+
+ let serializedServiceProperty = message.details[1].properties[0];
+
+ assert.equal(serializedServiceProperty.isService, true);
+ });
+
+ test("Computed property dependent keys and code should be successfully serialized", function(assert) {
+ let compuedFn = function() {
+ return this.get("foo") + this.get("bar");
+ };
+
+ let inspected = EmberObject.extend({
+ foo: true,
+ bar: false,
+ fooAndBar: computed("foo", "bar", compuedFn)
+ }).create();
+
+ objectInspector.sendObject(inspected);
+ let serializedComputedProperty = message.details[1].properties[2];
+
+ assert.equal(serializedComputedProperty.code, compuedFn.toString());
+ assert.equal(serializedComputedProperty.dependentKeys[0], "foo");
+ assert.equal(serializedComputedProperty.dependentKeys[1], "bar");
+ });
+
test('Read Only Computed properties mush have a readOnly property', function(assert) {
let inspected = EmberObject.extend({
readCP: computed(function() {}).property().readOnly(),