Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 37936200B81 for ; Tue, 13 Sep 2016 14:31:12 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id 36092160AD2; Tue, 13 Sep 2016 12:31:12 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 141E0160AC6 for ; Tue, 13 Sep 2016 14:31:09 +0200 (CEST) Received: (qmail 76238 invoked by uid 500); 13 Sep 2016 12:31:09 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 76229 invoked by uid 99); 13 Sep 2016 12:31:09 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 13 Sep 2016 12:31:09 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 1B31CDFD9F; Tue, 13 Sep 2016 12:31:09 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: robertkowalski@apache.org To: commits@couchdb.apache.org Date: Tue, 13 Sep 2016 12:31:09 -0000 Message-Id: <3c1ecfc1f6c845ba9b2f17a90f8db1b4@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [1/2] fauxton commit: updated refs/heads/master to 4f633e5 archived-at: Tue, 13 Sep 2016 12:31:12 -0000 Repository: couchdb-fauxton Updated Branches: refs/heads/master e51775b8a -> 4f633e508 react: port config section to react PR: #750 PR-URL: https://github.com/apache/couchdb-fauxton/pull/750 Reviewed-By: Robert Kowalski Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/ee1a92b0 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/ee1a92b0 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/ee1a92b0 Branch: refs/heads/master Commit: ee1a92b02e68d92dd86e5979d5d8f2d100fb24e7 Parents: e51775b Author: samk Authored: Sun Jul 24 21:55:11 2016 +1000 Committer: Robert Kowalski Committed: Tue Sep 13 14:22:41 2016 +0200 ---------------------------------------------------------------------- app/addons/config/actions.js | 121 ++++++ app/addons/config/actiontypes.js | 27 ++ app/addons/config/assets/less/config.less | 107 ++++- app/addons/config/components.react.jsx | 398 +++++++++++++++++++ app/addons/config/resources.js | 67 +--- app/addons/config/routes.js | 16 +- app/addons/config/stores.js | 149 +++++++ .../config/templates/add_config_option.html | 26 -- app/addons/config/templates/dashboard.html | 24 -- app/addons/config/templates/header.html | 15 - app/addons/config/templates/item.html | 40 -- app/addons/config/tests/actionsSpec.js | 193 +++++++++ .../config/tests/componentsSpec.react.jsx | 306 ++++++++++++++ app/addons/config/tests/configSpec.js | 162 -------- app/addons/config/tests/storesSpec.js | 94 +++++ app/addons/config/views.js | 229 ----------- devserver.js | 4 +- tasks/helper.js | 2 +- webpack.config.dev.js | 4 +- 19 files changed, 1411 insertions(+), 573 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/actions.js ---------------------------------------------------------------------- diff --git a/app/addons/config/actions.js b/app/addons/config/actions.js new file mode 100644 index 0000000..00cfc23 --- /dev/null +++ b/app/addons/config/actions.js @@ -0,0 +1,121 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import ActionTypes from './actiontypes'; +import FauxtonAPI from '../../core/api'; +import Resources from './resources'; + +export default { + fetchAndEditConfig (node) { + FauxtonAPI.dispatch({ type: ActionTypes.LOADING_CONFIG }); + + var configModel = new Resources.ConfigModel({ node }); + + configModel.fetch().then(() => this.editSections({ sections: configModel.get('sections'), node })); + }, + + editSections (options) { + FauxtonAPI.dispatch({ type: ActionTypes.EDIT_CONFIG, options }); + }, + + editOption (options) { + FauxtonAPI.dispatch({ type: ActionTypes.EDIT_OPTION, options }); + }, + + cancelEdit (options) { + FauxtonAPI.dispatch({ type: ActionTypes.CANCEL_EDIT, options }); + }, + + saveOption (node, options) { + FauxtonAPI.dispatch({ type: ActionTypes.SAVING_OPTION, options }); + + var modelAttrs = options; + modelAttrs.node = node; + var optionModel = new Resources.OptionModel(modelAttrs); + + optionModel.save() + .then(() => this.optionSaveSuccess(options)) + .fail(xhr => this.optionSaveFailure(options, JSON.parse(xhr.responseText).reason)); + }, + + optionSaveSuccess (options) { + FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_SUCCESS, options }); + FauxtonAPI.addNotification({ + msg: `Option ${options.optionName} saved`, + type: 'success' + }); + }, + + optionSaveFailure (options, error) { + FauxtonAPI.dispatch({ type: ActionTypes.OPTION_SAVE_FAILURE, options }); + FauxtonAPI.addNotification({ + msg: `Option save failed: ${error}`, + type: 'error' + }); + }, + + addOption (node, options) { + FauxtonAPI.dispatch({ type: ActionTypes.ADDING_OPTION }); + + var modelAttrs = options; + modelAttrs.node = node; + var optionModel = new Resources.OptionModel(modelAttrs); + + optionModel.save() + .then(() => this.optionAddSuccess(options)) + .fail(xhr => this.optionAddFailure(options, JSON.parse(xhr.responseText).reason)); + }, + + optionAddSuccess (options) { + FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_SUCCESS, options }); + FauxtonAPI.addNotification({ + msg: `Option ${options.optionName} added`, + type: 'success' + }); + }, + + optionAddFailure (options, error) { + FauxtonAPI.dispatch({ type: ActionTypes.OPTION_ADD_FAILURE, options }); + FauxtonAPI.addNotification({ + msg: `Option add failed: ${error}`, + type: 'error' + }); + }, + + deleteOption (node, options) { + FauxtonAPI.dispatch({ type: ActionTypes.DELETING_OPTION, options }); + + var modelAttrs = options; + modelAttrs.node = node; + var optionModel = new Resources.OptionModel(modelAttrs); + + optionModel.destroy() + .then(() => this.optionDeleteSuccess(options)) + .fail((xhr) => this.optionDeleteFailure(options, JSON.parse(xhr.responseText).reason)); + }, + + optionDeleteSuccess (options) { + FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_SUCCESS, options }); + FauxtonAPI.addNotification({ + msg: `Option ${options.optionName} deleted`, + type: 'success' + }); + }, + + optionDeleteFailure (options, error) { + FauxtonAPI.dispatch({ type: ActionTypes.OPTION_DELETE_FAILURE, options }); + FauxtonAPI.addNotification({ + msg: `Option delete failed: ${error}`, + type: 'error' + }); + } +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/actiontypes.js ---------------------------------------------------------------------- diff --git a/app/addons/config/actiontypes.js b/app/addons/config/actiontypes.js new file mode 100644 index 0000000..9391ac3 --- /dev/null +++ b/app/addons/config/actiontypes.js @@ -0,0 +1,27 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +export default { + EDIT_CONFIG: 'EDIT_CONFIG', + LOADING_CONFIG: 'LOADING_CONFIG', + EDIT_OPTION: 'EDIT_OPTION', + CANCEL_EDIT: 'CANCEL_EDIT', + SAVING_OPTION: 'SAVING_OPTION', + OPTION_SAVE_SUCCESS: 'OPTION_SAVE_SUCCESS', + OPTION_SAVE_FAILURE: 'OPTION_SAVE_FAILURE', + DELETING_OPTION: 'DELETING_OPTION', + OPTION_DELETE_SUCCESS: 'OPTION_DELETE_SUCCESS', + OPTION_DELETE_FAILURE: 'OPTION_DELETE_FAILURE', + ADDING_OPTION: 'ADDING_OPTION', + OPTION_ADD_SUCCESS: 'OPTION_ADD_SUCCESS', + OPTION_ADD_FAILURE: 'OPTION_ADD_FAILURE' +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/assets/less/config.less ---------------------------------------------------------------------- diff --git a/app/addons/config/assets/less/config.less b/app/addons/config/assets/less/config.less index cd57171..f74b2ec 100644 --- a/app/addons/config/assets/less/config.less +++ b/app/addons/config/assets/less/config.less @@ -9,31 +9,55 @@ // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. -// + @import "../../../../../assets/less/variables.less"; @import "../../../../../assets/less/bootstrap/mixins.less"; - - .config-item { height: 65px; - .js-value-input { + .config-value-form { + button { + margin-left: 8px; + } + } + + .config-value-input { width: 80%; margin: 0; } - .js-show-value, - .js-delete-value { + + .config-show-value, + .config-delete-value { cursor: pointer; } - .js-hidden { - display: none; - } + .text-center { text-align: center; } button { width: 7%;} + + transition: background-color 100ms; +} + +.config-item:hover { + background-color: #e0e0e0; +} + +.table-striped tbody > tr.config-item:nth-child(odd) > td, +.table-striped tbody > tr.config-item:nth-child(odd) > th { + transition: background-color 100ms; +} + +.table-striped tbody > tr.config-item:nth-child(odd):hover > td, +.table-striped tbody > tr.config-item:nth-child(odd):hover > th { + background-color: #e7e7e7; +} + +.config-item-deleting { + text-decoration: line-through; + text-decoration-color: red; } #config-trash { @@ -55,9 +79,14 @@ table.config { } } -#add-new-section { +#add-option-button { line-height: 40px; cursor: pointer; + border-left: 1px solid #ccc; + border-right: none; + border-top: none; + border-bottom: none; + padding: 12px 14px; &:hover { transition: none; @@ -67,8 +96,7 @@ table.config { } #add-section-button { - border-left: 1px solid #ccc; - padding: 12px 14px; + } .add-section-tray { @@ -112,6 +140,61 @@ table.config { } } +#add-option-popover { + background-color: #333; + color: white; + padding: 20px; + border-radius: 0; + width: 300px; + max-width: none; + display: block; + + .popover-content { + padding: 0; + } + + .popover-title { + font-size: 16px; + padding: 0; + margin: 10px 0; + border: none; + display: block; + text-align: left; + color: #fff; + text-shadow: none; + height: auto; + line-height: 1em; + background-color: #333; + } + + input { + width: 100%; + } + + a.btn { + color: white; + background-color: @linkColor; + line-height: 1.5em; + border: 0; + padding: 10px 10px 9px; + font-size: 14px; + .border-radius(5px); + + &:hover { + background-color: #cbcbcb; + color: white; + } + } +} + +.popover.bottom .arrow, .popover.bottom .arrow::after { + border-bottom-color: #333 !important; +} + +#add-option-popover::before { + display: none; +} + .config-warning-cluster-wrapper { .config-warning-cluster-container { margin-left: -50px; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/config/components.react.jsx b/app/addons/config/components.react.jsx new file mode 100644 index 0000000..a825282 --- /dev/null +++ b/app/addons/config/components.react.jsx @@ -0,0 +1,398 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import React from "react"; +import ReactDOM from "react-dom"; +import Stores from "./stores"; +import Actions from "./actions"; +import {Overlay, Button, Popover} from "react-bootstrap"; +import Components from "../components/react-components.react"; +import FauxtonComponents from "../fauxton/components.react"; + +const configStore = Stores.configStore; + +var ConfigTableController = React.createClass({ + getStoreState () { + return { + options: configStore.getOptions(), + loading: configStore.isLoading() + }; + }, + + getInitialState () { + return this.getStoreState(); + }, + + componentDidMount () { + configStore.on('change', this.onChange, this); + }, + + componentWillUnmount () { + configStore.off('change', this.onChange, this); + }, + + onChange () { + if (this.isMounted()) { + this.setState(this.getStoreState()); + } + }, + + saveOption (option) { + Actions.saveOption(this.props.node, option); + }, + + deleteOption (option) { + Actions.deleteOption(this.props.node, option); + }, + + editOption (option) { + Actions.editOption(option); + }, + + cancelEdit () { + Actions.cancelEdit(); + }, + + render () { + if (this.state.loading) { + return ( +
+ +
+ ); + } else { + return ( + + ); + } + } +}); + +var ConfigTable = React.createClass({ + createOptions () { + return _.map(this.props.options, (option) => ( + + )); + }, + + render () { + var options = this.createOptions(); + + return ( + + + + + + + + + + + {options} + +
SectionOptionValue
+ ); + } +}); + +var ConfigOption = React.createClass({ + onSave (value) { + var option = this.props.option; + option.value = value; + this.props.onSave(option); + }, + + onDelete () { + this.props.onDelete(this.props.option); + }, + + onEdit () { + this.props.onEdit(this.props.option); + }, + + render () { + return ( + + {this.props.option.header && this.props.option.sectionName} + {this.props.option.optionName} + + + + ); + } +}); + +var ConfigOptionValue = React.createClass({ + getInitialState () { + return { + value: this.props.value, + editing: this.props.editing, + saving: this.props.saving + }; + }, + + getDefaultProps () { + return { + value: '', + editing: false, + saving: false, + onSave: () => null, + onEdit: () => null, + onCancelEdit: () => null + }; + }, + + componentWillReceiveProps (nextProps) { + if (this.props.value !== nextProps.value) { + this.setState({ saving: false }); + } + }, + + onChange (event) { + this.setState({ value: event.target.value }); + }, + + onSave () { + if (this.state.value !== this.props.value) { + this.setState({ saving: true }); + this.props.onSave(this.state.value); + } else { + this.props.onCancelEdit(); + } + }, + + getButtons () { + if (this.state.saving) { + return null; + } else { + return ( + + + + ReactDOM.findDOMNode(this.refs.target)}> + {this.getPopover()} + + + ); + } +}); + +export default { + ConfigTableController: ConfigTableController, + ConfigTable: ConfigTable, + ConfigOption: ConfigOption, + ConfigOptionValue: ConfigOptionValue, + ConfigOptionTrash: ConfigOptionTrash, + AddOptionController: AddOptionController, + AddOptionButton: AddOptionButton +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/resources.js ---------------------------------------------------------------------- diff --git a/app/addons/config/resources.js b/app/addons/config/resources.js index 87eb8c3..f010c0d 100644 --- a/app/addons/config/resources.js +++ b/app/addons/config/resources.js @@ -15,26 +15,21 @@ import FauxtonAPI from "../../core/api"; var Config = FauxtonAPI.addon(); - Config.OptionModel = Backbone.Model.extend({ documentation: FauxtonAPI.constants.DOC_URLS.CONFIG, - initialize: function (_, options) { - this.node = options.node; - }, - - url: function () { - if (!this.node) { + url () { + if (!this.get('node')) { throw new Error('no node set'); } - return app.host + '/_node/' + this.node + '/_config/' + - this.get('section') + '/' + encodeURIComponent(this.get('name')); + return app.host + '/_node/' + this.get('node') + '/_config/' + + this.get('sectionName') + '/' + encodeURIComponent(this.get('optionName')); }, - isNew: function () { return false; }, + isNew () { return false; }, - sync: function (method, model, options) { + sync (method, model, options) { var params = { url: model.url(), @@ -52,58 +47,20 @@ Config.OptionModel = Backbone.Model.extend({ } }); -Config.Model = Backbone.Model.extend({}); -Config.Collection = Backbone.Collection.extend({ - model: Config.Model, - +Config.ConfigModel = Backbone.Model.extend({ documentation: FauxtonAPI.constants.DOC_URLS.CONFIG, - initialize: function (_, options) { - this.node = options.node; - }, - - comparator: function (OptionModel) { - if (OptionModel.get('section')) { - return OptionModel.get('section'); - } - }, - - url: function () { - if (!this.node) { + url () { + if (!this.get('node')) { throw new Error('no node set'); } - return app.host + '/_node/' + this.node + '/_config'; + return app.host + '/_node/' + this.get('node') + '/_config'; }, - findEntryInSection: function (sectionName, entry) { - var section = _.findWhere(this.toJSON(), {'section': sectionName}), - options; - - if (!section) { - return false; - } - - options = _.findWhere(section.options, {name: entry}); - - return options; - }, - - parse: function (resp) { - return _.map(resp, function (section, section_name) { - return { - section: section_name, - options: _.map(section, function (option, option_name) { - return { - name: option_name, - value: option - }; - }) - }; - }); + parse (resp) { + return { sections: resp }; } }); - - export default Config; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/routes.js ---------------------------------------------------------------------- diff --git a/app/addons/config/routes.js b/app/addons/config/routes.js index 7d9f210..bbdfe6a 100644 --- a/app/addons/config/routes.js +++ b/app/addons/config/routes.js @@ -17,6 +17,8 @@ import Views from "./views"; import CORSComponents from "../cors/components.react"; import CORSActions from "../cors/actions"; import ClusterActions from "../cluster/cluster.actions"; +import ConfigComponents from "./components.react"; +import ConfigActions from "./actions"; var ConfigDisabledRouteObject = FauxtonAPI.RouteObject.extend({ @@ -58,7 +60,7 @@ var ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({ initialize: function (_a, _b, options) { var node = options[0]; - this.configs = new Config.Collection(null, {node: node}); + this.configs = new Config.ConfigModel({ node: node }); this.sidebar = this.setView('#sidebar-content', new Views.Tabs({ sidebarItems: [ @@ -76,15 +78,17 @@ var ConfigPerNodeRouteObject = FauxtonAPI.RouteObject.extend({ })); }, - configForNode: function () { - this.newSection = this.setView('#right-header', new Views.ConfigHeader({ collection: this.configs })); - this.setView('#dashboard-lower-content', new Views.Table({ collection: this.configs })); + configForNode: function (node) { + this.removeComponents(); + this.setComponent('#right-header', ConfigComponents.AddOptionController, { node }); + this.setComponent('#dashboard-lower-content', ConfigComponents.ConfigTableController, { node }); + ConfigActions.fetchAndEditConfig(node); this.sidebar.setSelectedTab('main'); }, configCorsForNode: function (node) { - this.removeView('#right-header'); - this.newSection = this.setComponent('#dashboard-content', CORSComponents.CORSController); + this.removeComponents(); + this.setComponent('#dashboard-lower-content', CORSComponents.CORSController); CORSActions.fetchAndEditCors(node); this.sidebar.setSelectedTab('cors'); } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/config/stores.js b/app/addons/config/stores.js new file mode 100644 index 0000000..1c112d8 --- /dev/null +++ b/app/addons/config/stores.js @@ -0,0 +1,149 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import FauxtonAPI from '../../core/api'; +import ActionTypes from './actiontypes'; + +var ConfigStore = FauxtonAPI.Store.extend({ + initialize () { + this.reset(); + }, + + reset () { + this._sections = {}; + this._loading = true; + this._editSectionName = null; + this._editOptionName = null; + }, + + editConfig (sections) { + this._sections = sections; + this._loading = false; + this._editSectionName = null; + this._editOptionName = null; + }, + + getOptions () { + var sections = _.sortBy( + _.map(this._sections, function (section, sectionName) { + return { + sectionName, + options: this.mapSection(section, sectionName) + }; + }.bind(this)), + s => s.sectionName + ); + + return _.flatten(_.map(sections, s => s.options)); + }, + + mapSection (section, sectionName) { + var options = _.sortBy( + _.map(section, (value, optionName) => ({ + editing: this.isEditing(sectionName, optionName), + sectionName, optionName, value + })), o => o.optionName + ); + + options[0].header = true; + + return options; + }, + + editOption (sn, on) { + this._editSectionName = sn; + this._editOptionName = on; + }, + + isEditing (sn, on) { + return sn == this._editSectionName && on == this._editOptionName; + }, + + stopEditing () { + this._editOptionName = null; + this._editSectionName = null; + }, + + setLoading () { + this._loading = true; + }, + + isLoading () { + return this._loading; + }, + + saveOption (sectionName, optionName, value) { + if (!this._sections[sectionName]) { + this._sections[sectionName] = {}; + } + + this._sections[sectionName][optionName] = value || true; + }, + + deleteOption (sectionName, optionName) { + if (this._sections[sectionName]) { + delete this._sections[sectionName][optionName]; + + if (Object.keys(this._sections[sectionName]).length == 0) { + delete this._sections[sectionName]; + } + } + }, + + dispatch (action) { + if (action.options) { + var sectionName = action.options.sectionName; + var optionName = action.options.optionName; + var value = action.options.value; + } + + switch (action.type) { + case ActionTypes.EDIT_CONFIG: + this.editConfig(action.options.sections, action.options.node); + break; + + case ActionTypes.LOADING_CONFIG: + this.setLoading(); + break; + + case ActionTypes.EDIT_OPTION: + this.editOption(sectionName, optionName); + break; + + case ActionTypes.CANCEL_EDIT: + this.stopEditing(); + break; + + case ActionTypes.OPTION_SAVE_SUCCESS: + this.saveOption(sectionName, optionName, value); + this.stopEditing(); + break; + + case ActionTypes.OPTION_ADD_SUCCESS: + this.saveOption(sectionName, optionName, value); + break; + + case ActionTypes.OPTION_DELETE_SUCCESS: + this.deleteOption(sectionName, optionName); + break; + } + + this.triggerChange(); + } +}); + +var configStore = new ConfigStore(); +configStore.dispatchToken = FauxtonAPI.dispatcher.register(configStore.dispatch.bind(configStore)); + +export default { + configStore: configStore +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/templates/add_config_option.html ---------------------------------------------------------------------- diff --git a/app/addons/config/templates/add_config_option.html b/app/addons/config/templates/add_config_option.html deleted file mode 100644 index 13e36bd..0000000 --- a/app/addons/config/templates/add_config_option.html +++ /dev/null @@ -1,26 +0,0 @@ -<%/* -Licensed under the Apache License, Version 2.0 (the "License"); you may not -use this file except in compliance with the License. You may obtain a copy of -the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations under -the License. -*/%> - - Add Option - -
-
Add Option
- -
- - - - - Create -
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/templates/dashboard.html ---------------------------------------------------------------------- diff --git a/app/addons/config/templates/dashboard.html b/app/addons/config/templates/dashboard.html deleted file mode 100644 index 0af857d..0000000 --- a/app/addons/config/templates/dashboard.html +++ /dev/null @@ -1,24 +0,0 @@ -<%/* -Licensed under the Apache License, Version 2.0 (the "License"); you may not -use this file except in compliance with the License. You may obtain a copy of -the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations under -the License. -*/%> - - - - - - - - - - -
SectionOptionValue
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/templates/header.html ---------------------------------------------------------------------- diff --git a/app/addons/config/templates/header.html b/app/addons/config/templates/header.html deleted file mode 100644 index d2bd7fb..0000000 --- a/app/addons/config/templates/header.html +++ /dev/null @@ -1,15 +0,0 @@ -<%/* -Licensed under the Apache License, Version 2.0 (the "License"); you may not -use this file except in compliance with the License. You may obtain a copy of -the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations under -the License. -*/%> - -
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/templates/item.html ---------------------------------------------------------------------- diff --git a/app/addons/config/templates/item.html b/app/addons/config/templates/item.html deleted file mode 100644 index 71f0a64..0000000 --- a/app/addons/config/templates/item.html +++ /dev/null @@ -1,40 +0,0 @@ -<%/* -Licensed under the Apache License, Version 2.0 (the "License"); you may not -use this file except in compliance with the License. You may obtain a copy of -the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations under -the License. -*/%> - -<% if (option.index === 0) {%> - <%- option.section %> -<% } else { %> - -<% } %> - -
- <%- option.name %> -
-
- - - -
- - -
- <%- option.value %> -
-
- - - -
- - http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/tests/actionsSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/config/tests/actionsSpec.js b/app/addons/config/tests/actionsSpec.js new file mode 100644 index 0000000..0d35cae --- /dev/null +++ b/app/addons/config/tests/actionsSpec.js @@ -0,0 +1,193 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +import testUtils from "../../../../test/mocha/testUtils"; +import FauxtonAPI from "../../../core/api"; +import Actions from "../actions"; +import Resources from "../resources"; +import Backbone from "backbone"; +import sinon from "sinon"; + +const assert = testUtils.assert; +const restore = testUtils.restore; + +describe('Config Actions', function () { + var node = 'test'; + var option = { + sectionName: 'test', + optionName: 'test', + value: 'test' + }; + var failXhr = { responseText: '{}' }; + + describe('add', function () { + afterEach(function () { + restore(Actions.optionAddSuccess); + restore(Actions.optionAddFailure); + restore(FauxtonAPI.when); + restore(FauxtonAPI.addNotification); + restore(Backbone.Model.prototype.save); + }); + + it('calls optionAddSuccess when option add succeeds', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'save'); + var spy = sinon.spy(Actions, 'optionAddSuccess'); + var promise = FauxtonAPI.Deferred(); + promise.resolve(); + stub.returns(promise); + + Actions.addOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('shows notification when option add succeeds', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'save'); + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + var promise = FauxtonAPI.Deferred(); + promise.resolve(); + stub.returns(promise); + + Actions.addOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('calls optionAddFailure when option add fails', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'save'); + var spy = sinon.spy(Actions, 'optionAddFailure'); + var promise = FauxtonAPI.Deferred(); + promise.reject(failXhr); + stub.returns(promise); + + Actions.addOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('shows notification when option add fails', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'save'); + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + var promise = FauxtonAPI.Deferred(); + promise.reject(failXhr); + stub.returns(promise); + + Actions.addOption(node, option); + assert.ok(spy.calledOnce); + }); + }); + + describe('save', function () { + afterEach(function () { + restore(Actions.optionSaveSuccess); + restore(Actions.optionSaveFailure); + restore(FauxtonAPI.when); + restore(FauxtonAPI.addNotification); + restore(Backbone.Model.prototype.save); + }); + + it('calls optionSaveSuccess when option save succeeds', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'save'); + var spy = sinon.spy(Actions, 'optionSaveSuccess'); + var promise = FauxtonAPI.Deferred(); + promise.resolve(); + stub.returns(promise); + + Actions.saveOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('shows notification when option save succeeds', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'save'); + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + var promise = FauxtonAPI.Deferred(); + promise.resolve(); + stub.returns(promise); + + Actions.saveOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('calls optionSaveFailure when option save fails', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'save'); + var spy = sinon.spy(Actions, 'optionSaveFailure'); + var promise = FauxtonAPI.Deferred(); + promise.reject(failXhr); + stub.returns(promise); + + Actions.saveOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('shows notification when option save fails', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'save'); + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + var promise = FauxtonAPI.Deferred(); + promise.reject(failXhr); + stub.returns(promise); + + Actions.saveOption(node, option); + assert.ok(spy.calledOnce); + }); + }); + + describe('delete', function () { + afterEach(function () { + restore(Actions.optionDeleteSuccess); + restore(Actions.optionDeleteFailure); + restore(FauxtonAPI.when); + restore(FauxtonAPI.addNotification); + restore(Backbone.Model.prototype.destroy); + }); + + it('calls optionDeleteSuccess when option delete succeeds', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'destroy'); + var spy = sinon.spy(Actions, 'optionDeleteSuccess'); + var promise = FauxtonAPI.Deferred(); + promise.resolve(); + stub.returns(promise); + + Actions.deleteOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('shows notification when option delete succeeds', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'destroy'); + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + var promise = FauxtonAPI.Deferred(); + promise.resolve(); + stub.returns(promise); + + Actions.deleteOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('calls optionDeleteFailure when option delete fails', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'destroy'); + var spy = sinon.spy(Actions, 'optionDeleteFailure'); + var promise = FauxtonAPI.Deferred(); + promise.reject(failXhr); + stub.returns(promise); + + Actions.deleteOption(node, option); + assert.ok(spy.calledOnce); + }); + + it('shows notification when option delete fails', function () { + var stub = sinon.stub(Backbone.Model.prototype, 'destroy'); + var spy = sinon.spy(FauxtonAPI, 'addNotification'); + var promise = FauxtonAPI.Deferred(); + promise.reject(failXhr); + stub.returns(promise); + + Actions.deleteOption(node, option); + assert.ok(spy.calledOnce); + }); + }); +}); + http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/tests/componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/config/tests/componentsSpec.react.jsx b/app/addons/config/tests/componentsSpec.react.jsx new file mode 100644 index 0000000..5c49760 --- /dev/null +++ b/app/addons/config/tests/componentsSpec.react.jsx @@ -0,0 +1,306 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import FauxtonAPI from "../../../core/api"; +import Views from "../components.react"; +import Actions from "../actions"; +import Stores from "../stores"; +import utils from "../../../../test/mocha/testUtils"; +import React from "react"; +import ReactDOM from "react-dom"; +import TestUtils from "react-addons-test-utils"; +import sinon from "sinon"; + +FauxtonAPI.router = new FauxtonAPI.Router([]); +var assert = utils.assert; +var configStore = Stores.configStore; + +describe('Config Components', function () { + describe('ConfigTableController', function () { + var container, elm, node; + + beforeEach(function () { + container = document.createElement('div'); + configStore._loading = false; + configStore._sections = {}; + node = 'node2@127.0.0.1'; + elm = TestUtils.renderIntoDocument( + , + container + ); + }); + + afterEach(function () { + ReactDOM.unmountComponentAtNode(container); + }); + + it('deletes options', function () { + var spy = sinon.stub(Actions, 'deleteOption'); + var option = {}; + + elm.deleteOption(option); + assert.ok(spy.calledWith(node, option)); + }); + + it('saves options', function () { + var spy = sinon.stub(Actions, 'saveOption'); + var option = {}; + + elm.saveOption(option); + assert.ok(spy.calledWith(node, option)); + }); + + it('edits options', function () { + var spy = sinon.stub(Actions, 'editOption'); + var option = {}; + + elm.editOption(option); + assert.ok(spy.calledWith(option)); + }); + + it('cancels editing', function () { + var spy = sinon.stub(Actions, 'cancelEdit'); + + elm.cancelEdit(); + assert.ok(spy.calledOnce); + }); + }); + + describe('ConfigOption', function () { + var container; + + beforeEach(function () { + container = document.createElement('div'); + }); + + afterEach(function () { + ReactDOM.unmountComponentAtNode(container); + }); + + it('renders section name if the option is a header', function () { + var option = { + sectionName: 'test_section', + optionName: 'test_option', + value: 'test_value', + header: true + }; + + var el = TestUtils.renderIntoDocument(, container); + assert.equal($(ReactDOM.findDOMNode(el)).find('th').text(), 'test_section'); + }); + }); + + describe('ConfigOptionValue', function () { + var container; + + beforeEach(function () { + container = document.createElement('div'); + }); + + afterEach(function () { + ReactDOM.unmountComponentAtNode(container); + }); + + it('displays the value prop', function () { + var el = TestUtils.renderIntoDocument( + , container + ); + + assert.equal($(ReactDOM.findDOMNode(el)).text(), 'test_value'); + }); + + it('starts editing when clicked', function () { + var spy = sinon.spy(); + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el))[0]); + assert.ok(spy.calledOnce); + }); + + it('displays editing controls if editing', function () { + var el = TestUtils.renderIntoDocument( + , container + ); + + assert.equal($(ReactDOM.findDOMNode(el)).find('input.config-value-input').length, 1); + assert.equal($(ReactDOM.findDOMNode(el)).find('button.btn-config-cancel').length, 1); + assert.equal($(ReactDOM.findDOMNode(el)).find('button.btn-config-save').length, 1); + }); + + it('disables input when save clicked', function () { + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button.btn-config-save')[0]); + assert.equal($(ReactDOM.findDOMNode(el)).find('input.config-value-input').attr('disabled')); + }); + + it('saves changed value of input when save clicked', function () { + var change = { target: { value: 'new_value' } }; + var spy = sinon.spy(); + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.change($(ReactDOM.findDOMNode(el)).find('input.config-value-input')[0], change); + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button.btn-config-save')[0]); + assert.ok(spy.calledWith('new_value')); + }); + + it('cancels edit if save clicked with unchanged value', function () { + var spy = sinon.spy(); + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button.btn-config-save')[0]); + assert.ok(spy.calledOnce); + }); + }); + + describe('ConfigOptionTrash', function () { + var container; + + beforeEach(function () { + container = document.createElement('div'); + _.each($('div[data-reactroot]'), function (el) { + ReactDOM.unmountComponentAtNode(el.parentNode); + }); + }); + + afterEach(function () { + ReactDOM.unmountComponentAtNode(container); + }); + + it('displays delete modal when clicked', function () { + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el))[0]); + assert.equal($('div.confirmation-modal').length, 1); + }); + + it('calls on delete when confirmation modal Okay button clicked', function () { + var spy = sinon.spy(); + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el))[0]); + TestUtils.Simulate.click($('div.confirmation-modal button.btn-success')[0]); + assert.ok(spy.calledOnce); + }); + }); + + describe('AddOptionController', function () { + var container, elm; + + beforeEach(function () { + container = document.createElement('div'); + elm = TestUtils.renderIntoDocument( + , + container + ); + }); + + afterEach(function () { + ReactDOM.unmountComponentAtNode(container); + }); + + it('adds options', function () { + var spy = sinon.stub(Actions, 'addOption'); + + elm.addOption(); + assert.ok(spy.calledOnce); + }); + }); + + describe('AddOptionButton', function () { + var container; + + beforeEach(function () { + container = document.createElement('div'); + _.each($('div[data-reactroot]'), function (el) { + ReactDOM.unmountComponentAtNode(el.parentNode); + }); + }); + + afterEach(function () { + ReactDOM.unmountComponentAtNode(container); + }); + + it('displays add option controls when clicked', function () { + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]); + assert.equal($('div#add-option-popover .input-section-name').length, 1); + assert.equal($('div#add-option-popover .input-option-name').length, 1); + assert.equal($('div#add-option-popover .input-value').length, 1); + assert.equal($('div#add-option-popover .btn-create').length, 1); + }); + + it('does not hide popover if create clicked with invalid input', function () { + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]); + TestUtils.Simulate.click($('div#add-option-popover .btn-create')[0]); + assert.equal($('div#add-option-popover').length, 1); + }); + + it('does not add option if create clicked with invalid input', function () { + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]); + TestUtils.Simulate.click($('div#add-option-popover .btn-create')[0]); + assert.equal($('div#add-option-popover').length, 1); + }); + + + it('does adds option if create clicked with valid input', function () { + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]); + TestUtils.Simulate.click($('div#add-option-popover .btn-create')[0]); + assert.equal($('div#add-option-popover').length, 1); + }); + + it('adds option when create clicked with valid input', function () { + var spy = sinon.spy(); + var el = TestUtils.renderIntoDocument( + , container + ); + + TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('button#add-option-button')[0]); + TestUtils.Simulate.change($('div#add-option-popover .input-section-name')[0], { target: { value: 'test_section' } }); + TestUtils.Simulate.change($('div#add-option-popover .input-option-name')[0], { target: { value: 'test_option' } }); + TestUtils.Simulate.change($('div#add-option-popover .input-value')[0], { target: { value: 'test_value' } }); + TestUtils.Simulate.click($('div#add-option-popover .btn-create')[0]); + assert.ok(spy.calledWith(sinon.match({ + sectionName: 'test_section', + optionName: 'test_option', + value: 'test_value' + }))); + }); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/tests/configSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/config/tests/configSpec.js b/app/addons/config/tests/configSpec.js deleted file mode 100644 index 885ba24..0000000 --- a/app/addons/config/tests/configSpec.js +++ /dev/null @@ -1,162 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. -import FauxtonAPI from "../../../core/api"; -import Resources from "../resources"; -import Views from "../views"; -import testUtils from "../../../../test/mocha/testUtils"; -import sinon from "sinon"; -var assert = testUtils.assert, - ViewSandbox = testUtils.ViewSandbox, - collection; - -beforeEach(function () { - var optionModels = []; - - _.each([1, 2, 3], function (i) { - var model = new Resources.OptionModel({ - section: "foo" + i, - name: "bar" + i, - options: [{ - name: "testname" - }] - }, {node: 'foo'}); - - optionModels.push(model); - }); - - collection = new Resources.Collection(optionModels, {node: 'foo'}, "foo"); -}); - -describe("Config: Add Option Tray", function () { - var viewSandbox, - tray; - - beforeEach(function (done) { - tray = new Views.AddConfigOptionsButton({ - collection: collection - }); - - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(tray, done); - }); - - afterEach(function () { - viewSandbox.remove(); - }); - - it("looks if entries are new", function () { - tray.$('input[name="section"]').val("foo1"); - tray.$('input[name="name"]').val("testname"); - assert.ok(tray.isUniqueEntryInSection(collection)); - - tray.$('input[name="name"]').val("testname2"); - assert.notOk(tray.isUniqueEntryInSection(collection)); - }); - - it("does not send an error for a new section", function () { - tray.$('input[name="section"]').val("newsection"); - tray.$('input[name="name"]').val("testname"); - tray.$('input[name="value"]').val("testvalue"); - var spy = sinon.spy(tray, "showError"); - - tray.createConfigOption(); - assert.notOk(spy.called); - }); -}); - -describe("Config: Collection", function () { - it("looks if entries are new", function () { - assert.ok(collection.findEntryInSection("foo1", "testname")); - assert.notOk(collection.findEntryInSection("foo1", "testname2")); - }); - - it("returns false if findEntryInSection does not have the section", function () { - assert.notOk(collection.findEntryInSection("foo-not-exists", "testname")); - }); -}); - -describe("Config: TableRow", function () { - var tabMenu, optionModel; - - beforeEach(function () { - optionModel = new Resources.OptionModel({ - section: "foo", - name: "bar" - }, {node: 'foo'}); - - tabMenu = new Views.TableRow({ - model: optionModel, - uniqueName: function () { - return false; - } - }); - }); - - describe("editing Items", function () { - var viewSandbox; - beforeEach(function (done) { - viewSandbox = new ViewSandbox(); - viewSandbox.renderView(tabMenu, done); - }); - - afterEach(function () { - viewSandbox.remove(); - }); - - it("click on save should save the model and render", function () { - var renderSpy = sinon.stub(tabMenu, 'render'); - var saveSpy = sinon.stub(optionModel, 'save'); - - var $fields = tabMenu.$('.js-edit-value').filter(function (el) { - return $(this).find('[name="value"]').length; - }); - - $fields.find('.js-edit-value').trigger('dblclick'); - $fields.find('.js-save-value').trigger('click'); - - assert.ok(renderSpy.calledOnce); - assert.ok(saveSpy.calledOnce); - }); - - it("pressing enter should save the model and render", function () { - var renderSpy = sinon.stub(tabMenu, 'render'); - var saveSpy = sinon.stub(optionModel, 'save'); - - var e = $.Event("keyup"); - e.keyCode = 13; - - var $fields = tabMenu.$('.js-edit-value').filter(function (el) { - return $(this).find('[name="value"]').length; - }); - - $fields.find('.js-value-input').trigger(e); - - assert.ok(renderSpy.calledOnce); - assert.ok(saveSpy.calledOnce); - }); - - it("pressing Esc hides the field", function () { - var e = $.Event("keyup"); - e.keyCode = 27; - tabMenu.$('.js-value-input').trigger(e); - - assert.ok(tabMenu.$('.js-edit-value-form').hasClass('js-hidden')); - }); - - it("pressing Cancel hides the field", function () { - tabMenu.$('.js-edit-value').trigger('dblclick'); - tabMenu.$('.js-cancel-value').trigger('click'); - - assert.ok(tabMenu.$('.js-edit-value-form').hasClass('js-hidden')); - }); - }); -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/tests/storesSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/config/tests/storesSpec.js b/app/addons/config/tests/storesSpec.js new file mode 100644 index 0000000..a36ce12 --- /dev/null +++ b/app/addons/config/tests/storesSpec.js @@ -0,0 +1,94 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import Stores from '../stores'; +import utils from '../../../../test/mocha/testUtils'; + +var assert = utils.assert; + +describe("ConfigStore", function () { + var configStore = Stores.configStore; + + describe("mapSection", function () { + beforeEach(function () { + configStore._editOptionName = 'b'; + configStore._editSectionName = 'test'; + }); + + afterEach(function () { + configStore.reset(); + }); + + it("sorts options ascending", function () { + var options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test'); + assert.equal(options[0].optionName, 'a'); + }); + + it("sets the first option as the header", function () { + var options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test'); + assert.isTrue(options[0].header); + }); + + it("sets the option that is being edited", function () { + var options = configStore.mapSection({ b: 1, c: 2, a: 3 }, 'test'); + assert.isTrue(options[1].editing); + }); + }); + + describe("saveOption", function () { + var sectionName, optionName, value; + + beforeEach(function () { + sectionName = 'a'; + optionName = 'b'; + value = 1; + }); + + afterEach(function () { + configStore.reset(); + }); + + it("saves option to sections", function () { + configStore._sections = {}; + + configStore.saveOption(sectionName, optionName, value); + assert.deepEqual(configStore._sections, { a: { b: 1 } }); + }); + }); + + describe("deleteOption", function () { + var sectionName, optionName; + + beforeEach(function () { + sectionName = 'a'; + optionName = 'b'; + }); + + afterEach(function () { + configStore.reset(); + }); + + it("deletes option from section", function () { + configStore._sections = { a: { b: 1, c: 2 } }; + + configStore.deleteOption(sectionName, optionName); + assert.deepEqual(configStore._sections, { a: { c: 2 } }); + }); + + it("deletes section when all options are deleted", function () { + configStore._sections = { a: { b: 1 } }; + + configStore.deleteOption(sectionName, optionName); + assert.deepEqual(configStore._sections, {}); + }); + }); +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/app/addons/config/views.js ---------------------------------------------------------------------- diff --git a/app/addons/config/views.js b/app/addons/config/views.js index 398e396..c20b24a 100644 --- a/app/addons/config/views.js +++ b/app/addons/config/views.js @@ -10,238 +10,9 @@ // License for the specific language governing permissions and limitations under // the License. -import app from "../../app"; import FauxtonAPI from "../../core/api"; -import Config from "./resources"; -import Components from "../fauxton/components"; var Views = {}; -Views.TableRow = FauxtonAPI.View.extend({ - tagName: "tr", - className: "config-item", - template: "addons/config/templates/item", - events: { - "dblclick .js-edit-value": "editValue", - "click .js-delete-value": "deleteValue", - "click .js-cancel-value": "cancelEdit", - "click .js-save-value": "saveAndRender", - "keyup .js-value-input": "processKeyEvents" - }, - - deleteValue: function () { - var collection = this.collection, - result = confirm("Are you sure you want to delete this configuration value?"); - - if (!result) { return; } - - this.model.destroy().done(function () { - collection.fetch({reset: true}).done(function () { - FauxtonAPI.Events.trigger("config:rerender"); - }); - }); - - this.remove(); - }, - - editValue: function (event) { - this.$(event.currentTarget).find(".js-show-value").addClass("js-hidden"); - this.$(event.currentTarget).find(".js-edit-value-form").removeClass("js-hidden"); - this.$(event.currentTarget).find(".js-value-input").focus(); - }, - - processKeyEvents: function (event) { - // Enter key - if (event.keyCode === 13) { - return this.saveAndRender(event); - } - // Esc key - if (event.keyCode === 27) { - return this.discardValue(event); - } - }, - - discardValue: function (event) { - this.$(event.currentTarget).parents('td').find(".js-edit-value-form").addClass("js-hidden"); - this.$(event.currentTarget).parents('td').find(".js-show-value").removeClass("js-hidden"); - }, - - cancelEdit: function (event) { - this.discardValue(event); - }, - - serialize: function () { - return {option: this.model.toJSON()}; - }, - - saveAndRender: function (event) { - var options = {}, - $input = this.$(event.currentTarget).parents('td').find(".js-value-input"), - sectionName, - nameInSectionExists; - - options[$input.attr('name')] = $input.val(); - - if ($input.attr('name') === 'name') { - sectionName = this.model.get("section"); - nameInSectionExists = this.collection.findEntryInSection(sectionName, $input.val()); - if (nameInSectionExists) { - FauxtonAPI.addNotification({ - msg: "This config already exists, enter a unique name", - type: "error", - clear: true - }); - } else { - var newModel = this.model.clone(); - newModel.save(options); - this.model.destroy(); - this.model = newModel; - this.render(); - } - } else { - this.model.save(options); - this.render(); - } - } - -}); - -Views.Table = FauxtonAPI.View.extend({ - template: "addons/config/templates/dashboard", - - initialize: function () { - this.listenTo(FauxtonAPI.Events, "config:newSection", this.render); - this.listenTo(FauxtonAPI.Events, "config:rerender", this.render); - }, - - beforeRender: function () { - var collection = this.collection; - - this.collection.each(function (config) { - _.each(config.get("options"), function (option, index) { - this.insertView("table.config tbody", new Views.TableRow({ - collection: collection, - model: new Config.OptionModel({ - section: config.get("section"), - name: option.name, - value: option.value, - index: index - }, {node: this.collection.node}) - })); - }, this); - }, this); - }, - - establish: function () { - return [this.collection.fetch()]; - } -}); - -Views.ConfigHeader = FauxtonAPI.View.extend({ - template: 'addons/config/templates/header', - className: 'header-right', - - initialize: function () { - this.rightHeader = this.setView('#add-section-button', new Views.AddConfigOptionsButton({ collection: this.collection })); - } -}); - - -Views.AddConfigOptionsButton = Components.Tray.extend({ - template: 'addons/config/templates/add_config_option', - - events: { - 'click #js-create-config-section': 'createConfigOption' - }, - - initialize: function () { - this.initTray({ toggleTrayBtnSelector: '#add-new-section' }); - }, - - processKey: function (e) { - if (e.which === 13) { - e.preventDefault(); - this.createConfigOption(); - } - }, - - afterRender: function () { - this.sectionNames = _.map(this.collection.toJSON(), function (item) { - return item.section; - }); - - this.sectionTypeAhead = new Components.Typeahead({ - source: this.sectionNames, - el: 'input[name="section"]' - }); - this.sectionTypeAhead.render(); - }, - - createConfigOption: function (e) { - if (e) { - e.preventDefault(); - } - - var section = this.$('input[name="section"]').val(), - name = this.$('input[name="name"]').val(), - value = this.$('input[name="value"]').val(), - collection = this.collection; - - // perform a little validation, then submit - if (!section) { - this.showError('Please enter or select a section.'); - } else if (!name) { - this.showError('Please add an option name.'); - } else if (this.isUniqueEntryInSection(collection)) { - this.showError('The option have a unique name.'); - } else if (!value) { - this.showError('Please add a value.'); - } else { - this.submitForm(); - } - }, - - submitForm: function () { - - var option = new Config.OptionModel({ - section: this.$('input[name="section"]').val(), - name: this.$('input[name="name"]').val(), - value: this.$('input[name="value"]').val() - }, {node: this.collection.node}); - option.save(); - - var section = this.collection.find(function (section) { - return section.get('section') === option.get('section'); - }); - - if (section) { - section.get('options').push(option.attributes); - } else { - this.collection.add({ - section: option.get('section'), - options: [option.attributes] - }); - } - - this.hideTray(); - FauxtonAPI.Events.trigger('config:newSection'); - }, - - isUniqueEntryInSection: function (collection) { - var sectionName = this.$('input[name="section"]').val(), - entry = this.$('input[name="name"]').val(); - - return collection.findEntryInSection(sectionName, entry); - }, - - showError: function (msg) { - FauxtonAPI.addNotification({ - msg: msg, - type: 'error', - clear: true - }); - } -}); - Views.Tabs = FauxtonAPI.View.extend({ className: "sidenav", tagName: "nav", http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/devserver.js ---------------------------------------------------------------------- diff --git a/devserver.js b/devserver.js index 3bef0a8..be2c9d6 100644 --- a/devserver.js +++ b/devserver.js @@ -1,4 +1,4 @@ -var spawn = require('child_process').spawn; +var spawn = require('cross-spawn').spawn; var path = require("path"); var fs = require("fs"); var _ = require('lodash'); @@ -18,7 +18,7 @@ var loadSettings = function () { port: process.env.FAUXTON_PORT || 8000, contentSecurityPolicy: true, proxy: { - target: 'http://127.0.0.1:5984', + target: 'http://couch:5984', changeOrigin: false } }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/tasks/helper.js ---------------------------------------------------------------------- diff --git a/tasks/helper.js b/tasks/helper.js index a995c23..696e1c0 100644 --- a/tasks/helper.js +++ b/tasks/helper.js @@ -14,7 +14,7 @@ var fs = require('fs'), path = require('path'); exports.devServerPort = 8000; -exports.couch = 'http://localhost:5984/'; +exports.couch = 'http://couch:5984/'; exports.init = function (grunt) { var _ = grunt.util._, http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/ee1a92b0/webpack.config.dev.js ---------------------------------------------------------------------- diff --git a/webpack.config.dev.js b/webpack.config.dev.js index edb4833..05f7b92 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -73,5 +73,7 @@ module.exports = { path: __dirname + '/dist/debug', publicPath: '/', filename: 'bundle.js' //All our code is compiled into a single file called bundle.js - } + }, + + devtool: 'source-map' };