From 05779c03f070bb16990ab0fc050098a8b695ba7a Mon Sep 17 00:00:00 2001 From: annyhe Date: Fri, 10 Feb 2017 15:10:07 -0800 Subject: [PATCH] edit perspectives (#236) * implement edit perspective * wont go into another page * removed field checking * remove old functions * fix minor bug * update style to fix messed up indentation --- public/css/perspective.css | 8 - tests/view/components/createPerspective.js | 241 +++++++++++++++----- tests/view/components/dropdown.js | 24 +- tests/view/components/persController.js | 57 +++++ tests/view/components/perspectiveStatic.js | 138 ++++++----- tests/view/components/utils.js | 41 ++++ view/admin/components/common/Dropdown.js | 69 +++--- view/perspective/CreatePerspective.js | 164 +++++++------ view/perspective/PerspectiveController.js | 44 ++-- view/perspective/app.js | 47 +--- view/perspective/configCreatePerspective.js | 131 ++++++++--- 11 files changed, 649 insertions(+), 315 deletions(-) create mode 100644 tests/view/components/persController.js create mode 100644 tests/view/components/utils.js 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, };