diff --git a/public/css/perspective.css b/public/css/perspective.css
index 137ae5e430..5fbf1cd3e5 100644
--- a/public/css/perspective.css
+++ b/public/css/perspective.css
@@ -87,10 +87,6 @@ body {
margin: -5px 0 0 0;
}
-.slds-lookup__item-action.slds-media.highlighted {
- background: #e0e5ee;
-}
-
/* radio button width */
.slds-modal .slds-grid {
@@ -127,10 +123,6 @@ body {
padding-top: 100;
}
-.slds-dropdown__item {
- overflow: scroll;
-}
-
.slds-dropdown {
overflow: scroll;
margin-left: 5px;
diff --git a/tests/view/components/createPerspective.js b/tests/view/components/createPerspective.js
index 73aba941e6..a4d51340ac 100644
--- a/tests/view/components/createPerspective.js
+++ b/tests/view/components/createPerspective.js
@@ -13,14 +13,17 @@
import { expect } from 'chai';
import React from 'react';
import sinon from 'sinon';
-import CreatePerspective from '../../../view/perspective/CreatePerspective.js';
+import { getSubjects } from './utils';
+import CreatePerspective from '../../../view/perspective/CreatePerspective';
import { mount } from 'enzyme';
describe('Perspective view ', () => {
const ZERO = 0;
const ONE = 1;
const TWO = 2;
+ const EMPTY_STR = '';
const DUMMY_STRING = 'COOL';
+ const DUMMY_ID = '743bcf42-cd79-46d0-8c0f-d43adbb63866';
const DUMMY_FUNCTION = () => {};
const ONE_SUBJECT = {
absolutePath: DUMMY_STRING,
@@ -28,6 +31,12 @@ describe('Perspective view ', () => {
};
const DUMMY_ARRAY = 'qwertyui'.split('');
const { getDropdownStyle } = CreatePerspective;
+ const LENS = {
+ id: DUMMY_ID,
+ name: DUMMY_STRING,
+ isPublished: true,
+ };
+ const PERS_NAME = DUMMY_STRING;
/**
* Sets up the component with dummy prop values.
@@ -37,90 +46,172 @@ describe('Perspective view ', () => {
* overrides to the default props
* @returns {Object} The rendered component
*/
- function setup(valuesAddons, stateAddons) {
+ function setup(valuesAddons, otherPropsObj) {
// simulate loading config
const defaultProps = {
+ name: PERS_NAME,
+ params: {},
cancelCreate: DUMMY_FUNCTION,
- sendResource: DUMMY_FUNCTION,
+ isEditing: false,
+ sendResource: spy,
+ // options or all possible values
values: {
- aspectFilter: [],
- aspectTags: [],
- lenses: [],
- name: DUMMY_STRING,
- perspectives: [],
- statusFilter: [],
+ aspectTagFilter: [],
subjectTagFilter: [],
- subjects: [],
+ lenses: [LENS],
+ // actual values
+ perspectives: [{
+ name: PERS_NAME,
+ lens: LENS,
+ rootSubject: DUMMY_STRING,
+ aspectFilterType: "EXCLUDE",
+ aspectFilter: [ ],
+ aspectTagFilterType: "EXCLUDE",
+ aspectTagFilter: [ ],
+ subjectTagFilterType: "EXCLUDE",
+ subjectTagFilter: [ ], // empty for testing
+ statusFilterType: "EXCLUDE",
+ statusFilter: DUMMY_ARRAY, // not empty for testing
+ }],
},
- stateObject: {
- perspectives: [],
- subjects: [],
- lenses: [],
- statusFilterType: '',
- statusFilter: [],
- subjectTagFilter: [],
- subjectTagFilterType: '',
- aspectTagFilter: [],
- aspectTagFilterType: '',
- aspectFilter: [],
- aspectFilterType: '',
- }
};
// update defaultProps as needed
if (valuesAddons) {
Object.assign(defaultProps.values, valuesAddons);
}
- if (stateAddons) {
- Object.assign(defaultProps.stateAddons, stateAddons);
+ if (otherPropsObj) {
+ Object.assign(defaultProps, otherPropsObj)
}
+
// use monut to test all lifecycle methods, and children
const enzymeWrapper = mount();
return enzymeWrapper;
}
let stub;
+ let spy;
beforeEach(() => {
stub = sinon.stub(CreatePerspective, 'findCommonAncestor');
+ spy = sinon.spy();
});
afterEach(() => {
CreatePerspective.findCommonAncestor.restore();
});
- it('given the proper url parameter and resource, ' +
- ' create modal is shown with proper resource name', () => {
- const enzymeWrapper = setup();
- expect(enzymeWrapper.find('.slds-modal__container')).to.have.length(ONE);
- expect(enzymeWrapper.find('.slds-text-heading--medium').text())
- .to.equal('New Perspective');
+ describe('after setting props isEditing to true', () => {
+ it('sendResource first argument is PUT', () => {
+ const enzymeWrapper = setup(null, { isEditing: true });
+ const instance = enzymeWrapper.instance();
+ instance.doCreate();
+ expect(spy.calledOnce).to.be.true;
+ // expect method to be PUT
+ expect(spy.args[0][0]).to.equal('PUT');
+ });
+
+ it('sendResource form object argument has field url defined, ' +
+ 'does not end with perspectives', () => {
+ const enzymeWrapper = setup(null, { isEditing: true });
+ const instance = enzymeWrapper.instance();
+ instance.doCreate();
+ expect(spy.calledOnce).to.be.true;
+ const formObj = spy.args[0][1];
+ expect(formObj).to.to.be.an('object');
+ expect(formObj.url).to.be.defined;
+ // expect url to end with perspective name
+ expect(formObj.url.split('/').pop()).to.equal(DUMMY_STRING);
+ });
});
- it('options are loaded from props', () => {
- const enzymeWrapper = setup({
- subjects: [ONE_SUBJECT], //
+ describe('on create', () => {
+ it('on create, state is set to params values', () => {
+ const params = {
+ 'subjects': 'NorthAmerica',
+ 'lenses': 'MultiTable',
+ 'statusFilterType': 'INCLUDE',
+ 'statusFilter': ['OK'],
+ 'subjectTagFilterType': 'EXCLUDE',
+ 'subjectTagFilter': [],
+ 'aspectTagFilterType': 'INCLUDE',
+ 'aspectTagFilter': ['OK'],
+ 'aspectFilterType': 'EXCLUDE',
+ 'aspectFilter': []
+ }
+ const enzymeWrapper = setup({}, { params });
+ const instance = enzymeWrapper.instance();
+ for (let key in params) {
+ expect(instance.state[key]).to.equal(params[key]);
+ }
+ });
+
+ it('dropdown options still contains all the lenses,' +
+ ' even though state lens is empty', () => {
+ // be default, not editing
+ const enzymeWrapper = setup({});
+ const instance = enzymeWrapper.instance();
+ expect(instance.state.lenses).to.equal(EMPTY_STR);
+ // the lens dropdown is not empty
+ expect(instance.state.dropdownConfig.lenses.options.length).to.equal(ONE);
+ });
+
+ it('empty lens means state lens is also empty', () => {
+ // add onto default
+ const values = {
+ // actual values
+ lenses: [],
+ };
+ const enzymeWrapper = setup(values);
+ const instance = enzymeWrapper.instance();
+ // the current lens is empty
+ expect(instance.state.lenses).to.equal(EMPTY_STR);
+ });
+
+ it('empty array means state array is also empty', () => {
+ const values = {
+ aspectTagFilter: [],
+ };
+ const enzymeWrapper = setup(values);
+ const instance = enzymeWrapper.instance();
+ expect(instance.state.aspectTagFilter).to.deep.equal([]);
});
- const instance = enzymeWrapper.instance();
- instance.updateDropdownConfig();
- const config = instance.state.dropdownConfig;
- expect(Object.keys(config)).to.contain('subjects');
- expect(config.subjects.options.length).to.equal(ONE);
});
- it('on filter, options array is alphabetical', () => {
- const enzymeWrapper = setup({
- statusFilter: DUMMY_ARRAY,
+ describe('on initial render', () => {
+ it('on Create, output name is empty', () => {
+ const enzymeWrapper = setup();
+ const instance = enzymeWrapper.instance();
+ const INPUT = enzymeWrapper.find('input[name="name"]');
+ expect(INPUT.getNode().value).to.equal(EMPTY_STR);
+ });
+
+ it('on edit, perspective name is not empty');
+
+ it('props maps to state', () => {
+ const enzymeWrapper = setup({}, { isEditing: true });
+ const instance = enzymeWrapper.instance();
+ expect(instance.state.statusFilter).to.deep.equal(DUMMY_ARRAY);
+ });
+
+ it('initial props isEditing is false', () => {
+ const enzymeWrapper = setup();
+ const instance = enzymeWrapper.instance();
+ expect(instance.props.isEditing).to.be.false;
+ });
+
+ it('on edit, subject options are not empty', () => {
+ const enzymeWrapper = setup({}, { isEditing: true });
+ const instance = enzymeWrapper.instance();
+ expect(instance.state.subjects).to.equal(DUMMY_STRING);
+ // one value, no leftover options
+ expect(instance.state.dropdownConfig.subjects.options.length).to.equal(ZERO);
});
- const instance = enzymeWrapper.instance();
- instance.updateDropdownConfig();
- const config = instance.state.dropdownConfig;
- expect(Object.keys(config)).to.contain('statusFilter');
- expect(config.statusFilter.options.length).to.equal(DUMMY_ARRAY.length);
});
- it('state includes default perspective name', () => {
+ it('given the proper url parameter and resource, ' +
+ ' create modal is shown with proper resource name', () => {
const enzymeWrapper = setup();
- const instance = enzymeWrapper.instance();
- expect(Object.keys(instance.state)).to.contain('perspectiveName');
- expect(instance.state.perspectiveName).to.equal('');
+ expect(enzymeWrapper.find('.slds-modal__container')).to.have.length(ONE);
+ expect(enzymeWrapper.find('.slds-text-heading--medium').text())
+ .to.equal('New Perspective');
});
it('on state change, perspective name is perserved', () => {
@@ -156,7 +247,9 @@ describe('Perspective view ', () => {
for (let key in config) {
const styleObj = getDropdownStyle(instance.state, key);
expect(styleObj.hasOwnProperty('marginTop')).to.be.true;
- expect(styleObj.marginTop).to.equal(ZERO);
+ if (!config[key].isArray) {
+ expect(styleObj.marginTop).to.equal(ZERO);
+ }
}
});
@@ -223,6 +316,28 @@ describe('Perspective view ', () => {
.to.contain(DUMMY_STRING);
});
+ it('on remove pill, state is updated for single pill input', () => {
+ const enzymeWrapper = setup();
+ const RESOURCE_NAME = 'subjects';
+ const instance = enzymeWrapper.instance();
+ instance.setState({ subjects: [ONE_SUBJECT] });
+ const OBJ = {
+ textContent: DUMMY_STRING,
+ };
+ // for pillElem
+ stub.withArgs(OBJ, 'slds-pill').returns({
+ getElementsByClassName: () => 'subjects'
+ });
+ // for fieldElem
+ stub.withArgs(OBJ, 'slds-form-element__control')
+ .returns({ title: 'subjects' });
+
+ instance.deletePill({
+ target: OBJ,
+ });
+ expect(instance.state[RESOURCE_NAME]).to.equal('');
+ });
+
it('on remove pill, dropdown style does not change', () => {
const enzymeWrapper = setup();
const RESOURCE_NAME = 'subjects';
@@ -249,7 +364,7 @@ describe('Perspective view ', () => {
describe('for array inputs', () => {
it('onclck remove pill, margin top is re-adjusted', () => {
- const RESOURCE_NAME = 'aspectFilter';
+ const RESOURCE_NAME = 'aspectTagFilter';
const enzymeWrapper = setup({
RESOURCE_NAME
});
@@ -279,7 +394,7 @@ describe('Perspective view ', () => {
it('onclck remove pill, the removed option is added ' +
'back into the dropdown', () => {
- const RESOURCE_NAME = 'aspectFilter';
+ const RESOURCE_NAME = 'aspectTagFilter';
const enzymeWrapper = setup();
const instance = enzymeWrapper.instance();
const OBJ = {
@@ -307,15 +422,15 @@ describe('Perspective view ', () => {
const enzymeWrapper = setup();
const instance = enzymeWrapper.instance();
const config = instance.state.dropdownConfig;
- for (let key in config) {
- const styleObj = getDropdownStyle(instance.state, key);
- expect(styleObj.hasOwnProperty('marginTop')).to.be.true;
- expect(styleObj.marginTop).to.equal(ZERO);
- }
+ const key = 'subjectTagFilter';
+ const styleObj = getDropdownStyle(instance.state, key);
+ expect(styleObj.hasOwnProperty('marginTop')).to.be.true;
+ // check margin top of empty values
+ expect(styleObj.marginTop).to.equal(ZERO);
});
it('on pill removal, margin top moves up', () => {
- const RESOURCE_NAME = 'aspectFilter';
+ const RESOURCE_NAME = 'aspectTagFilter';
const setupObj = {};
const enzymeWrapper = setup(setupObj[RESOURCE_NAME]: DUMMY_ARRAY);
const instance = enzymeWrapper.instance();
@@ -339,10 +454,10 @@ describe('Perspective view ', () => {
it('isArray property is true', () => {
const enzymeWrapper = setup({
- aspectFilter: 'qwewretrytuyi'.split(''),
+ aspectTagFilter: 'qwewretrytuyi'.split(''),
});
const instance = enzymeWrapper.instance();
- expect(instance.state.dropdownConfig.aspectFilter.isArray).to.be.true;
+ expect(instance.state.dropdownConfig.aspectTagFilter.isArray).to.be.true;
});
it('appending pills updates state to array of length one', () => {
@@ -392,7 +507,7 @@ describe('Perspective view ', () => {
},
});
expect(getDropdownStyle(instance.state, RESOURCE_NAME).marginTop)
- .to.equal(instance.props.BLOCK_SIZE);
+ .to.be.above(ZERO);
});
it('on add two pills, dropdown style moves down to accomodate pill', () => {
diff --git a/tests/view/components/dropdown.js b/tests/view/components/dropdown.js
index 419aa9daff..6e9ffe0fdf 100644
--- a/tests/view/components/dropdown.js
+++ b/tests/view/components/dropdown.js
@@ -55,6 +55,26 @@ describe('Dropdown component tests', () => {
return enzymeWrapper;
}
+ it('state data loads from props options', () => {
+ const ARR = [DUMMY_STRING];
+ const enzymeWrapper = setup({ options: ARR });
+ const instance = enzymeWrapper.instance();
+ expect(instance.state.data.length).to.equal(ONE);
+ expect(instance.state.data).to.deep.equal(ARR);
+ });
+
+ it('given an array of single element, render single item', () => {
+ const enzymeWrapper = setup({ options: [DUMMY_STRING] });
+ const instance = enzymeWrapper.instance();
+ // change to open state, to show dropdown
+ instance.setState({ open: true });
+ expect(enzymeWrapper.find('.slds-dropdown__item')).to.have.length(ONE);
+ });
+
+ it('on props showEditIcon true, render pencil icon');
+ it('by default showEditIcon is false');
+ it('on showEditIcon is false, no pencil icon is rendered');
+
it('on toggle true, dropdown opens', () => {
const enzymeWrapper = setup();
const instance = enzymeWrapper.instance();
@@ -84,10 +104,10 @@ describe('Dropdown component tests', () => {
});
it('the INPUT has no value, and there are options, ' +
- 'the first cell is highlighted', () => {
+ 'the first cell is NOT highlighted', () => {
const enzymeWrapper = setup({ options: DUMMY_ARRAY, defaultValue: '' });
const instance = enzymeWrapper.instance();
- expect(instance.state.highlightedIndex).to.equal(ZERO);
+ expect(instance.state.highlightedIndex).to.equal(-1);
});
it('the INPUT has value, the highlighted index ' +
diff --git a/tests/view/components/persController.js b/tests/view/components/persController.js
new file mode 100644
index 0000000000..a75334ec1a
--- /dev/null
+++ b/tests/view/components/persController.js
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2016, salesforce.com, inc.
+ * All rights reserved.
+ * Licensed under the BSD 3-Clause license.
+ * For full license text, see LICENSE.txt file in the repo root or
+ * https://opensource.org/licenses/BSD-3-Clause
+ */
+
+/**
+ * tests/view/components/persController.js
+ */
+
+import { expect } from 'chai';
+import React from 'react';
+import sinon from 'sinon';
+import PerspectiveController from '../../../view/perspective/PerspectiveController.js';
+import { mount } from 'enzyme';
+
+describe('Perspective controller ', () => {
+ const ZERO = 0;
+ const ONE = 1;
+ const TWO = 2;
+ const DUMMY_STRING = 'COOL';
+ const DUMMY_ID = '743bcf42-cd79-46d0-8c0f-d43adbb63866';
+ const DUMMY_FUNCTION = () => {};
+ const ONE_SUBJECT = {
+ absolutePath: DUMMY_STRING,
+ isPublished: true,
+ };
+ const DUMMY_ARRAY = 'qwertyui'.split('');
+ const LENS = {
+ id: DUMMY_ID,
+ name: DUMMY_STRING,
+ };
+
+ function setup() {
+ const defaultProps = {
+ params: {},
+ values: {
+ aspectFilter: [],
+ aspectTags: [],
+ lenses: [LENS],
+ },
+ }
+ const enzymeWrapper = mount();
+ return enzymeWrapper;
+ }
+
+ describe('on show create modal', () => {
+ it('calling openCreatePanel sets state to true', () => {
+ const enzymeWrapper = setup();
+ const instance = enzymeWrapper.instance();
+ instance.openCreatePanel();
+ expect(instance.state.showCreatePanel).to.be.true;
+ });
+ });
+});
diff --git a/tests/view/components/perspectiveStatic.js b/tests/view/components/perspectiveStatic.js
index 67df15c7b0..8945eb55b3 100644
--- a/tests/view/components/perspectiveStatic.js
+++ b/tests/view/components/perspectiveStatic.js
@@ -11,72 +11,104 @@
*/
import { expect } from 'chai';
-import CreatePerspective from '../../../view/perspective/CreatePerspective.js';
-import { getArray } from '../../../view/perspective/configCreatePerspective.js';
+import { getArray,
+ getTagsFromResources,
+ getConfig,
+ filteredArray,
+ getOptions,
+} from '../../../view/perspective/configCreatePerspective';
+import { getSubjects } from './utils';
-describe('Test static functions', () => {
+describe('Config perspective functions', () => {
const ZERO = 0;
+ const NUM = 10;
const POPULAR_SAYING = 'The quick brown fox jumps over the lazy dog';
+ const ARR = POPULAR_SAYING.split(' ');
+ const WORD = 'fox';
- /**
- * Returns an array of resources with identical
- * isPublished property, with
- * fieldName field == index in loop
- *
- * @param {Integer} INT Make this many resources
- * @param {String} fieldName The field of each resource
- * @param {Boolean} isPublished All resources have
- * this value of isPublished
- * @returns {Array} Array with all published resources
- */
- function getSubjects(INT, fieldName, isPublished) {
- let subjects = [];
- for (let i = INT; i > ZERO; i--) {
- const obj = {
- isPublished,
- absolutePath: i,
- };
- obj[fieldName] = i;
- subjects.push(obj);
- }
- return subjects;
- }
-
- it('getArray returns only published resources', () => {
- const NUM = 10;
- const unPublished = getArray(
- 'absolutePath',
- getSubjects(NUM, 'absolutePath')
- );
- expect(unPublished.length).to.be.empty;
-
- const published = getArray(
- 'absolutePath',
- getSubjects(NUM, 'absolutePath', true)
- );
- expect(published.length).to.equal(NUM);
- // input is in decreasing order
- // should preserve order
- expect(published[ZERO]).to.equal(NUM);
+ it('getTagsFromResources does not return duplicates', () => {
+ const arr = [
+ { tags: [ WORD ] },
+ { tags: [] },
+ { tags: [ WORD ] },
+ ];
+ const resultArr = getTagsFromResources(arr);
+ expect(resultArr.length).to.equal(1);
+ expect(resultArr).to.deep.equal([WORD]);
});
- it('getArray should preserve order of input resources', () => {
- const NUM = 10;
- const published = getArray(
- 'absolutePath',
- getSubjects(NUM, 'absolutePath', true),
+ it('dropdown removes all options, if values === options', () => {
+ // remove empty spaces
+ const arr = getOptions(
+ ARR,
+ ARR,
);
- // input is in decreasing order
- expect(published[ZERO]).to.equal(NUM);
+ expect(arr).to.be.empty;
});
- it('on select option, dropdown removes that option ' +
+ it('dropdown removes existing option ' +
'from available options', () => {
// remove empty spaces
- const filteredArray = CreatePerspective.filteredArray(
+ const arr = filteredArray(
POPULAR_SAYING.split(''),
' ',
);
- expect(filteredArray).to.not.contain(' ');
+ expect(arr).to.not.contain(' ');
+ });
+
+ describe('getConfig', () => {
+ it('config options contain the expected number of options', () => {
+ const key = 'statusFilter'; // any string
+ const values = {};
+ values[key] = POPULAR_SAYING.split(' ');
+ const value = [WORD];
+ const config = getConfig(values, key, value);
+ expect(config.options.length).to.equal(values[key].length - 1);
+ });
+
+ it('options contain only values not in field', () => {
+ const key = 'statusFilter'; // any string
+ const values = {};
+ values[key] = POPULAR_SAYING.split(' ');
+ const value = [WORD];
+ const config = getConfig(values, key, value);
+ expect(config.options).to.not.contain(WORD);
+ });
+ });
+
+ describe('getArray', () => {
+ it('returns all items except ' +
+ 'for the item whose field === third param key');
+
+ it('does not return unPublished resources', () => {
+ const unPublished = getArray(
+ 'absolutePath',
+ // unpublished
+ getSubjects(NUM, 'absolutePath', false)
+ );
+ expect(unPublished.length).to.be.empty;
+ });
+
+ it('returns published resources', () => {
+ const published = getArray(
+ 'absolutePath',
+ // published
+ getSubjects(NUM, 'absolutePath', true)
+ );
+ expect(published.length).to.equal(NUM);
+ // input is in decreasing order
+ // should preserve order
+ expect(published[ZERO]).to.equal(NUM);
+ });
+
+ it('preserves the order of input resources', () => {
+ const published = getArray(
+ 'absolutePath',
+ // published
+ getSubjects(NUM, 'absolutePath', true),
+ );
+ // input is in decreasing order
+ expect(published[ZERO]).to.equal(NUM);
+ });
});
});
diff --git a/tests/view/components/utils.js b/tests/view/components/utils.js
new file mode 100644
index 0000000000..3726a43240
--- /dev/null
+++ b/tests/view/components/utils.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2016, salesforce.com, inc.
+ * All rights reserved.
+ * Licensed under the BSD 3-Clause license.
+ * For full license text, see LICENSE.txt file in the repo root or
+ * https://opensource.org/licenses/BSD-3-Clause
+ */
+
+/**
+ * tests/view/components/utils.js
+ */
+
+const ZERO = 0;
+
+/**
+ * Returns an array of resources with identical
+ * isPublished property, with
+ * fieldName field == index in loop
+ *
+ * @param {Integer} INT Make this many resources
+ * @param {String} fieldName The field of each resource
+ * @param {Boolean} isPublished All resources have
+ * this value of isPublished
+ * @returns {Array} Array with all published resources
+ */
+function getSubjects(INT, fieldName, isPublished) {
+ let subjects = [];
+ for (let i = INT; i > ZERO; i--) {
+ const obj = {
+ isPublished,
+ absolutePath: i,
+ };
+ obj[fieldName] = i;
+ subjects.push(obj);
+ }
+ return subjects;
+}
+
+module.exports = {
+ getSubjects,
+};
diff --git a/view/admin/components/common/Dropdown.js b/view/admin/components/common/Dropdown.js
index 93252c34fa..adf9afc612 100644
--- a/view/admin/components/common/Dropdown.js
+++ b/view/admin/components/common/Dropdown.js
@@ -41,10 +41,6 @@ function getIndexFromArray(options, defaultValue) {
let index = -ONE; // default: no options in dropdown
if (options.length) {
index = options.indexOf(defaultValue);
- // indexOf might return 0
- if (index < ZERO) {
- index = ZERO;
- }
}
return index;
}
@@ -74,11 +70,12 @@ class Dropdown extends React.Component {
super(props);
this.state = {
open: false, // dropdown is open or closed
- data: [], // data in dropdown
- highlightedIndex: getIndexFromArray(props.options, props. defaultValue),
+ data: props.options, // data in dropdown
+ highlightedIndex: getIndexFromArray(props.options, props.defaultValue),
};
this.toggle = this.toggle.bind(this);
}
+
componentDidMount() {
// click anywhere outside of container
// to hide dropdown
@@ -100,6 +97,7 @@ class Dropdown extends React.Component {
open: bool,
});
}
+
handleFocus() {
// show all options
this.setState({
@@ -107,6 +105,7 @@ class Dropdown extends React.Component {
data: this.props.options,
});
}
+
handleKeyUp(evt) {
const Key = {
UP: 38,
@@ -136,7 +135,7 @@ class Dropdown extends React.Component {
false,
),
});
- } else if (keycode === Key.ENTER) {
+ } else if (keycode === Key.ENTER && this.props.renderAsLink) {
const persName = options[highlightedIndex];
window.location.href = '/perspectives/' + persName;
} else {
@@ -160,6 +159,7 @@ class Dropdown extends React.Component {
}
render () {
const {
+ options,
newButtonText,
dropDownStyle,
allOptionsLabel,
@@ -168,37 +168,48 @@ class Dropdown extends React.Component {
showSearchIcon,
onAddNewButton,
onClickItem,
+ onEdit,
showInputElem,
children, // react elements
defaultValue,
+ showEditIcon,
+ renderAsLink, //boolean
} = this.props;
const { data } = this.state;
let outputUL = '';
// if options exist, load them
if (data.length) {
- outputUL =
+ outputUL =
{data.map((optionsName, index) => {
- let className = 'slds-lookup__item-action ' +
- 'slds-media slds-media--center';
+ // by default do not redirect page onclick
+ const link = renderAsLink ? '/perspectives/' + optionsName : 'javascript:void(0)';
+ let listClassName = 'slds-dropdown__item';
if (index === this.state.highlightedIndex) {
- className += ' highlighted';
+ listClassName += ' slds-is-selected';
+ }
+ const itemOutput = !renderAsLink ? optionsName :
+ { optionsName }
+ ;
+ // TODO: refactor to get selected item out of props
+ return -
+
+
+ { optionsName }
+ {showEditIcon &&
+ }
+
+
}
- return (
- -
-
-
- { optionsName }
-
-
- );
- }
)}
;
}
@@ -224,7 +235,7 @@ class Dropdown extends React.Component {
className={'slds-form-element__control ' +
'slds-grid slds-wrap slds-grid--pull-padded'}
>
-
+
{ !children && inputElem}
{ children }
{ (children && showInputElem) && inputElem }
@@ -281,11 +292,13 @@ Dropdown.propTypes = {
placeholderText: PropTypes.string,
defaultValue: PropTypes.string,
onAddNewButton: PropTypes.func,
+ onEdit: PropTypes.func,
onClickItem: PropTypes.func.isRequired,
children: PropTypes.element,
showSearchIcon: PropTypes.bool,
showInputElem: PropTypes.bool,
close: PropTypes.bool, // if true, close dropdown
+ renderAsLink: PropTypes.bool, // render list item as link
};
export default Dropdown;
diff --git a/view/perspective/CreatePerspective.js b/view/perspective/CreatePerspective.js
index f58f6106d8..b0f412d64d 100644
--- a/view/perspective/CreatePerspective.js
+++ b/view/perspective/CreatePerspective.js
@@ -18,7 +18,7 @@ import Dropdown from '../admin/components/common/Dropdown';
import ControlledInput from '../admin/components/common/ControlledInput';
import ErrorRender from '../admin/components/common/ErrorRender';
import RadioGroup from '../admin/components/common/RadioGroup';
-import { getConfig } from './configCreatePerspective';
+import { filteredArray, getConfig } from './configCreatePerspective';
const ZERO = 0;
@@ -35,21 +35,6 @@ function getStateDataOnly(stateObject) {
return stateCopy;
}
-/**
- * Ie. 'thisStringIsGood' --> This String Is Good
- * @param {String} string The string to split
- * @returns {String} The converted string, includes spaces.
- */
-function convertCamelCase(string) {
- return string
- // insert a space before all caps
- .replace(/([A-Z])/g, ' $1')
- // uppercase the first character
- .replace(/^./, function(str) {
- return str.toUpperCase();
- });
-}
-
class CreatePerspective extends React.Component {
// separate props and status from value prop
constructor(props) {
@@ -62,27 +47,20 @@ class CreatePerspective extends React.Component {
this.state = {
dropdownConfig: {},
error: '',
- perspectiveName: '',
- ...props.stateObject,
+ name: props.name,
+ subjects: [],
+ lenses: '',
+ statusFilterType: '',
+ statusFilter: [],
+ subjectTagFilter: [],
+ subjectTagFilterType: '',
+ aspectTagFilter: [],
+ aspectTagFilterType: '',
+ aspectFilter: [],
+ aspectFilterType: '',
}; // default values
}
- /**
- * Given array of objects, returns array without
- * the input elements
- *
- * @param {Array} arr The array to filter from
- * @param {String} removeThis The elem to remove from array.
- * Multiple elements may be removed
- * get new array from
- * @returns {Array} The array of strings or primitives
- */
- static filteredArray(arr, removeThis) {
- return arr.filter((elem) => {
- return elem !== removeThis;
- });
- }
-
/**
* @param {DOM_element} el The element to find ancestor with selector from
* @param {String} selector The selector of ancestor
@@ -112,34 +90,68 @@ class CreatePerspective extends React.Component {
}
componentDidMount() {
- this.updateDropdownConfig();
+ const { values, name, isEditing, params } = this.props;
+ if (isEditing) {
+ // operating on a named, saved perspective
+ if (values && Array.isArray(values.perspectives) && values.perspectives.length) {
+ const perspective = values.perspectives.filter((pers) => pers.name === name)[0];
+ console.log('perspective is', perspective)
+ this.setState({
+ // defaults
+ name,
+ lenses: perspective.lens.name || '',
+ subjects: perspective.rootSubject || '',
+ statusFilterType: perspective.statusFilterType || 'EXCLUDE',
+ statusFilter: perspective.statusFilter || [],
+ subjectTagFilter: perspective.subjectTagFilter || [],
+ subjectTagFilterType: perspective.subjectTagFilterType || 'EXCLUDE',
+ aspectTagFilter: perspective.aspectTagFilter || [],
+ aspectTagFilterType: perspective.aspectTagFilterType || 'EXCLUDE',
+ aspectFilter: perspective.aspectFilter || [],
+ aspectFilterType: perspective.aspectFilterType || 'EXCLUDE',
+ }, () => {
+ this.updateDropdownConfig(perspective);
+ });
+ }
+ } else {
+ // unnamed perspective defined in url
+ this.setState({
+ // defaults
+ name: '',
+ lenses: params.lenses || '',
+ subjects: params.subjects || '',
+ statusFilterType: params.statusFilterType || 'EXCLUDE',
+ statusFilter: params.statusFilter || [],
+ subjectTagFilter: params.subjectTagFilter || [],
+ subjectTagFilterType: params.subjectTagFilterType || 'EXCLUDE',
+ aspectTagFilter: params.aspectTagFilter || [],
+ aspectTagFilterType: params.aspectTagFilterType || 'EXCLUDE',
+ aspectFilter: params.aspectFilter || [],
+ aspectFilterType: params.aspectFilterType || 'EXCLUDE',
+ }, () => {
+ this.updateDropdownConfig(params);
+ });
+ }
}
- updateDropdownConfig() {
+
+ updateDropdownConfig(perspective) {
// attach config to keys, keys to dropdownConfig
const { dropdownConfig } = this.state;
- const { values } = this.props;
- let stateObject = getStateDataOnly(this.state);
- let config = {};
+ const { values, BLOCK_SIZE } = this.props;
+ let stateObject = getStateDataOnly(this.state);
for (let key in stateObject) {
- const value = this.state[key];
- const convertedText = convertCamelCase(key);
- config = {
- title: key,
- defaultValue: Array.isArray(value) ?
- value.join('') : value,
- options: values[key] || [],
- showSearchIcon: false,
- onClickItem: this.appendPill,
- dropDownStyle: { marginTop: 0 },
- };
-
- const result = getConfig(values, key, value, convertedText);
- // combine default config with special config for each resource
- config = Object.assign(config, result);
+ let value = this.state[key]; //default
+ // if perspective passed in, may amend value based on key
+ let config = getConfig(values, key, value);
+ // if this dropdown is multi-pill, move the dropdown menu lower
+ let marginTop = !config.isArray ? ZERO : value.length * BLOCK_SIZE;
+ config.dropDownStyle = { marginTop },
+ config.onClickItem = this.appendPill,
dropdownConfig[key] = config;
}
- this.setState({ dropdownConfig });
+
+ this.setState({dropdownConfig });
}
handleRadioButtonClick(event) {
@@ -179,7 +191,6 @@ class CreatePerspective extends React.Component {
deletePill(event) {
const {
findCommonAncestor,
- filteredArray,
getDropdownStyle,
} = this.constructor;
const pillElem = findCommonAncestor(event.target, 'slds-pill');
@@ -206,7 +217,9 @@ class CreatePerspective extends React.Component {
newState[dropdownTitle] = '';
}
// add selected option to available options in dropdown
- newState.dropdownConfig[dropdownTitle].options.push(labelContent);
+ if (newState.dropdownConfig[dropdownTitle].options.indexOf(labelContent) < 0) {
+ newState.dropdownConfig[dropdownTitle].options.push(labelContent);
+ }
// sort in-place by alphabetical order.
newState.dropdownConfig[dropdownTitle].options.sort();
this.setState(newState);
@@ -215,7 +228,6 @@ class CreatePerspective extends React.Component {
appendPill(event) {
const {
findCommonAncestor,
- filteredArray,
} = this.constructor;
const { BLOCK_SIZE } = this.props;
const valueToAppend = event.target.textContent;
@@ -253,14 +265,15 @@ class CreatePerspective extends React.Component {
this.setState(newState);
}
+ // POST or PUT, depending on state
doCreate() {
- const { values, sendResource } = this.props;
+ const { values, sendResource, isEditing, name } = this.props;
const postObject = getStateDataOnly(this.state);
if (!postObject.lenses.length) {
this.showError('Please enter a valid lens.');
} else if (!postObject.subjects.length) {
this.showError('Please enter a valid subject.');
- } else if (!postObject.perspectives.length) {
+ } else if (!postObject.name.length) {
this.showError('Please enter a name for this perspective.');
} else {
// check if lens field is uid. if not, need to get uid for lens name
@@ -273,26 +286,30 @@ class CreatePerspective extends React.Component {
this.showError('Please enter a valid lens name. No lens with name '
+ postObject.lenses + ' found');
}
-
postObject.lenses = lens[ZERO].id;
}
// for create perspectives, rename key lenses --> lensId,
// and perspectives --> name. Start with deep copy values obj
postObject.lensId = postObject.lenses;
postObject.rootSubject = postObject.subjects;
- postObject.name = postObject.perspectives;
delete postObject.lenses;
delete postObject.subjects;
- delete postObject.perspectives;
// go to created perspective page
- sendResource('POST', postObject, this.showError);
+ let method = 'POST'; // default
+ postObject.url = '/v1/perspectives';
+ if (isEditing) {
+ method = 'PUT';
+ // use the original perspective name
+ postObject.url = postObject.url + '/' + name
+ }
+ sendResource(method, postObject, this.showError);
}
}
render() {
- const { cancelCreate } = this.props;
+ const { cancelCreate, isEditing } = this.props;
let dropdownObj = {};
- const { dropdownConfig } = this.state;
+ const { dropdownConfig, name } = this.state;
const radioGroupConfig = {};
const accountIcon =
@@ -334,7 +351,7 @@ class CreatePerspective extends React.Component {
/>;
}
}
- const _config = Object.assign(dropdownConfig[key], { showInputElem });
+ const _config = Object.assign({}, dropdownConfig[key], { showInputElem });
dropdownObj[key] = (
{ pillOutput }
@@ -345,13 +362,14 @@ class CreatePerspective extends React.Component {
hide={this.closeError.bind(this)}
error={ this.state.error } /> :
' ';
+
return (
*Name
@@ -455,8 +473,8 @@ CreatePerspective.propTypes = {
cancelCreate: PropTypes.func,
sendResource: PropTypes.func,
values: PropTypes.object,
- stateObject: PropTypes.object,
- BLOCK_SIZE: PropTypes.string,
+ parms: PropTypes.object,
+ BLOCK_SIZE: PropTypes.number,
};
// the pixel amount to move dropdown up or down
CreatePerspective.defaultProps = { BLOCK_SIZE: 25 };
diff --git a/view/perspective/PerspectiveController.js b/view/perspective/PerspectiveController.js
index 65271de46e..465872d8d5 100644
--- a/view/perspective/PerspectiveController.js
+++ b/view/perspective/PerspectiveController.js
@@ -23,13 +23,16 @@ class PerspectiveController extends React.Component {
super(props);
this.sendResource = this.sendResource.bind(this);
this.state = {
+ name: props.values.name, // perspective name
+ isEditing: false,
showCreatePanel: false,
+ isCreating: false,
showEditPanel: false,
};
}
sendResource(verb, formObj, errCallback) {
new Promise((resolve, reject) => {
- request(verb, '/v1/perspectives')
+ request(verb, formObj.url)
.set('Content-Type', 'application/json')
.set('Authorization', u.getCookie('Authorization'))
.send(JSON.stringify(formObj))
@@ -43,44 +46,53 @@ class PerspectiveController extends React.Component {
errCallback(err);
});
}
- goToUrl(event) {
- window.location.href = '/perspectives/' + event.target.textContent;
- }
+ // TODO: test this is independent of onEdit
openCreatePanel() {
- this.setState({ showCreatePanel: true });
+ this.setState({ isEditing: false, isCreating: true, showCreatePanel: true });
}
cancelForm() {
this.setState({ showCreatePanel: false });
}
+ onEdit(event) {
+ // prevent the page from refreshing to another perspective
+ event.preventDefault();
+ // TODO: refactor to get from onclick handler, instead of through DOM
+ const name = event.target.parentElement.parentElement.textContent;
+ // update values according to name
+ this.setState({ isEditing: true, name, showCreatePanel: true });
+ }
+
render() {
- const { values, stateObject } = this.props;
+ const { values, params } = this.props;
+ const { showCreatePanel, isEditing, name } = this.state;
let persNames = [];
if (values && values.perspectives) {
persNames = values.perspectives.map((persObject) => {
return persObject.name;
});
}
- // to hide perspective name on createPerspective modal,
- // set perspectives key to value empty
- const createPerspectiveVal = JSON.parse(JSON.stringify(stateObject));
- createPerspectiveVal.perspectives = '';
return (
- { this.state.showCreatePanel && }
@@ -89,8 +101,8 @@ class PerspectiveController extends React.Component {
}
PerspectiveController.PropTypes = {
+ // contains perspective, subjects, ...
values: PropTypes.object,
- stateObject: PropTypes.object,
};
export default PerspectiveController;
diff --git a/view/perspective/app.js b/view/perspective/app.js
index b2edc74127..78038584e9 100644
--- a/view/perspective/app.js
+++ b/view/perspective/app.js
@@ -48,6 +48,7 @@ import request from 'superagent';
import React from 'react';
import ReactDOM from 'react-dom';
import PerspectiveController from './PerspectiveController';
+import { getTagsFromResources } from './configCreatePerspective';
const u = require('../utils');
const eventsQueue = require('./eventsQueue');
let gotLens = false;
@@ -255,7 +256,7 @@ function getFilterQuery(p) {
}
const sign = p.aspectTagFilterType === 'INCLUDE' ? '' : '-';
- q += 'aspectTags' + '=' + sign +
+ q += 'aspectTagFilter' + '=' + sign +
p.aspectTagFilter.join().replace(/,/g, ',' + sign);
}
@@ -397,30 +398,6 @@ function getAllParams() {
return responseObject;
} // getAllParams
-/**
- * Returns array of objects with tags
- * @param {Array} array The array of reosurces to get tags from.
- * @returns {Object} array of tags
- */
-function getTagsFromResources(array) {
- // get all tags
- const allTags = [];
- array.map((obj) => {
- if (obj.tags.length) {
- allTags.push(...obj.tags);
- }
- });
- const tagNames = [];
-
- // get through tags, get all names
- allTags.map((tagObj) => {
- if (tagNames.indexOf(tagObj.toLowerCase()) === -1) {
- tagNames.push(tagObj);
- }
- });
- return tagNames;
-}
-
function getPublishedObjectsbyField(array, field) {
return array.filter((obj) =>
obj.isPublished).map((obj) => obj[field])
@@ -431,14 +408,10 @@ function getPublishedObjectsbyField(array, field) {
*/
function loadPerspective(perspective, params) {
pcValues.name = perspective.name;
- const stateObject = Object.assign(
- { perspectives: perspective ? perspective.name : '' },
- params
- );
getPromiseWithUrl('perspectives', '/v1/perspectives')
.then((values) => {
pcValues.perspectives = values.res;
- loadController(pcValues, stateObject);
+ loadController(pcValues, params);
});
} // loadPerspective
@@ -451,10 +424,6 @@ function loadPerspective(perspective, params) {
function loadExtraStuffForCreatePerspective(perspective, params, promisesArr,
getRoot, getLens) {
pcValues.name = perspective.name;
- const stateObject = Object.assign(
- { perspectives: perspective ? perspective.name : '' },
- params
- );
const pArr = promisesArr || [];
const getAllSubjectsPromise = getPromiseWithUrl('subjects', '/v1/subjects');
@@ -508,9 +477,9 @@ function loadExtraStuffForCreatePerspective(perspective, params, promisesArr,
}
pcValues.statusFilter = statusFilter;
- pcValues.aspectTags = getTagsFromResources(pcValues.aspectFilter);
+ pcValues.aspectTagFilter = getTagsFromResources(pcValues.aspectFilter);
pcValues.subjectTagFilter = getTagsFromResources(pcValues.subjects);
- loadController(pcValues, stateObject);
+ loadController(pcValues, params);
});
} // loadExtraStuffForCreatePerspective
@@ -637,13 +606,13 @@ if (_realtimeEventThrottleMilliseconds !== ZERO) {
* Passes data on to Controller to pass onto renderers.
*
* @param {Object} values Data returned from AJAX.
- * @param {Object} stateObject Data from queryParams.
+ * @param {Object} params Data from queryParams.
*/
-function loadController(values, stateObject) {
+function loadController(values, params) {
ReactDOM.render(
,
PERSPECTIVE_CONTAINER
);
diff --git a/view/perspective/configCreatePerspective.js b/view/perspective/configCreatePerspective.js
index 9d0886260e..8914618065 100644
--- a/view/perspective/configCreatePerspective.js
+++ b/view/perspective/configCreatePerspective.js
@@ -13,7 +13,7 @@
*/
/**
* Given array of objects, returns array of strings or primitives
- * of values of the field key
+ * of arrayOfObjects[i][field].
*
* @param {String} field The field of each value to return
* @param {array} arrayOfObjects The array of objects to
@@ -33,26 +33,106 @@ function getArray(field, arrayOfObjects) {
return arr;
}
+
+/**
+ * Ie. 'thisStringIsGood' --> This String Is Good
+ * @param {String} string The string to split
+ * @returns {String} The converted string, includes spaces.
+ */
+function convertCamelCase(string) {
+ return string
+ // insert a space before all caps
+ .replace(/([A-Z])/g, ' $1')
+ // uppercase the first character
+ .replace(/^./, function(str) {
+ return str.toUpperCase();
+ });
+}
+
+/**
+ * Given array of objects, returns array without
+ * the input elements
+ *
+ * @param {Array} arr The array to filter from
+ * @param {String} removeThis The elem to remove from array.
+ * Multiple elements may be removed
+ * get new array from
+ * @returns {Array} The array of strings or primitives
+ */
+function filteredArray(arr, removeThis) {
+ return arr.filter((elem) => {
+ return elem !== removeThis;
+ });
+}
+
+
+/**
+ * Returns array of objects with tags
+ * @param {Array} array The array of reosurces to get tags from.
+ * @returns {Object} array of tags
+ */
+function getTagsFromResources(array) {
+ // get all tags
+ let cumulativeArr = [];
+ for (var i = array.length - 1; i >= 0; i--) {
+ if (array[i].tags.length) {
+ cumulativeArr.push(...array[i].tags);
+ }
+ }
+
+ return cumulativeArr.filter((item, pos) => {
+ return cumulativeArr.indexOf(item) !== pos;
+ });
+}
+
+/**
+ * Return array of items that are from one array and
+ * not in another
+ *
+ * @param {Array} options Return a subset of this
+ * @param {Array} value Array of data to exclude
+ * @returns {Array} Contains items from options
+ */
+function getOptions(options, value) {
+ let leftovers = []; // populate from options
+ if (Array.isArray(value)) {
+ for (var i = options.length - 1; i >= 0; i--) {
+ if (value.indexOf(options[i]) < 0) {
+ leftovers.push(options[i]);
+ }
+ }
+ }
+ return leftovers;
+}
+
/**
* Returns config object for the key in values array.
*
* @param {Array} values Data to get resource config.
* From props
* @param {String} key The key of the resource, in values array
- * @param {Array} value Current state's values
- * @param {String} convertedText For resource config
+ * @param {Array} value Update state to this value
* @returns {Object} The resource configuration object
*/
-function getConfig(values, key, value, convertedText) {
- const config = {};
+function getConfig(values, key, value) {
const ZERO = 0;
+ const options = getOptions(values[key] || [], value);
+ const convertedText = convertCamelCase(key);
+ let config = {
+ title: key,
+ options,
+ showSearchIcon: false,
+ };
+
if (key === 'subjects') {
- config.options = getArray('absolutePath', values[key]);
+ let options = getArray('absolutePath', values[key]);
+ config.options = filteredArray(options, value);
config.placeholderText = 'Select a Subject...';
config.isArray = false;
} else if (key === 'lenses') {
config.placeholderText = 'Select a Lens...';
- config.options = getArray('name', values[key]);
+ let options = getArray('name', values[key]);
+ config.options = filteredArray(options, value);
config.isArray = false;
} else if (key.slice(-6) === 'Filter') {
// if key ends with Filter
@@ -60,41 +140,26 @@ function getConfig(values, key, value, convertedText) {
config.allOptionsLabel = 'All ' +
convertedText.replace(' Filter', '') + 's';
config.isArray = true;
- if (key === 'aspectFilter') {
- config.options = getArray('name', values[key]);
- config.allOptionsLabel = 'All ' +
- convertedText.replace(' Filter', '') + ' Tags';
- } else if (key === 'statusFilter') {
+ if (key === 'statusFilter') {
config.allOptionsLabel = 'All ' +
convertedText.replace(' Filter', '') + 'es';
+ } else if (key === 'aspectFilter') {
+ config.allOptionsLabel = 'All ' +
+ convertedText.replace(' Filter', '') + 's';
+ let options = getArray('name', values[key]);
+ config.options = getOptions(options, value);
+ console.log(key, values[key], options, value, config.options)
}
delete config.placeholderText;
- // remove value[i] if not in all appropriate values
- let notAllowedTags = [];
- for (let i = ZERO; i < value.length; i++) {
- if (!values[key] || values[key].indexOf(value[i]) < ZERO) {
- notAllowedTags.push(value[i]);
- }
- }
- if (notAllowedTags.length) {
- // remove from state
- const newVals = value.filter((item) => {
- return notAllowedTags.indexOf(item) < ZERO;
- });
- const errorMessage = ' ' + convertedText + ' ' +
- notAllowedTags.join(', ') + ' does not exist.';
- const stateRule = {
- error: errorMessage
- };
- stateRule[key] = newVals;
- this.setState(stateRule);
- }
}
return config;
}
export {
+ getOptions, // for testing
+ filteredArray,
getConfig,
- getArray
+ getArray,
+ getTagsFromResources,
};