diff --git a/resources/frontend_client/app/admin/datasets/datasets.controllers.js b/resources/frontend_client/app/admin/datasets/datasets.controllers.js index 0ba777f3f04ead1649fe4397baa9be8d69905b11..54035640cc7d8498963453bd9371783e071010bb 100644 --- a/resources/frontend_client/app/admin/datasets/datasets.controllers.js +++ b/resources/frontend_client/app/admin/datasets/datasets.controllers.js @@ -40,7 +40,7 @@ AdminDatasetsControllers.controller('AdminDatasetList', ['$scope', 'Metabase', f }]); -AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams', '$location', 'Metabase', function($scope, $routeParams, $location, Metabase) { +AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams', '$location', 'Metabase', 'ForeignKey', function($scope, $routeParams, $location, Metabase, ForeignKey) { $scope.annotations = []; $scope.fields = []; @@ -54,20 +54,13 @@ AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams }, }; - Metabase.table_get({ + + Metabase.table_query_metadata({ 'tableId': $routeParams.tableId }, function(result) { $scope.table = result; - - // get the fields for this table - Metabase.table_fields({ - 'tableId': $routeParams.tableId - }, function(result) { - $scope.fields = result; - }); - - // table annotations - + $scope.getIdFields(); + $scope.decorateWithTargets(); }, function(error) { console.log(error); if (error.status == 404) { @@ -75,6 +68,32 @@ AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams } }); + $scope.getIdFields = function(){ + // fetch the ID fields + Metabase.db_idfields({ + 'dbId': $scope.table.db.id + }, function(result) { + if (result && !result.error) { + $scope.idfields = result; + result.forEach(function(field) { + field.displayName = field.table.name + '.' + field.name; + }); + } else { + console.log(result); + } + }); + + }; + + $scope.decorateWithTargets = function(){ + console.log($scope.table); + $scope.table.fields.forEach(function(field){ + if (field.target){ + field.target_id = field.target.id; + } + }); + }; + $scope.syncMetadata = function () { Metabase.table_sync_metadata({ 'tableId': $routeParams.tableId @@ -97,17 +116,73 @@ AdminDatasetsControllers.controller('AdminDatasetEdit', ['$scope', '$routeParams } }; + $scope.inlineSpecialTypeChange = function(idx){ + // If we are changing the field from a FK to something else, we should delete any FKs present + var field = $scope.table.fields[idx]; + if (field.target_id && field.special_type != "fk"){ + // we have something that used to be an FK and is now not an FK + // Let's delete its foreign keys + var fks = Metabase.field_foreignkeys({'fieldId': field.id}, function(result){ + fks.forEach(function(fk){ + console.log("deleting ", fk); + ForeignKey.delete({'fkID': fks[0].id}, function(result){ + console.log("deleted fk"); + }, function(error){ + console.log("error deleting fk"); + }); + }); + }); + // clean up after ourselves + field.target = null; + field.target_id = null; + } + // save the field + $scope.inlineSaveField(idx); + }; + $scope.inlineSaveField = function(idx) { - if ($scope.fields && $scope.fields[idx]) { - Metabase.field_update($scope.fields[idx], function(result) { + if ($scope.table.fields && $scope.table.fields[idx]) { + Metabase.field_update($scope.table.fields[idx], function(result) { if (result && !result.error) { - $scope.fields[idx] = result; + $scope.table.fields[idx] = result; } else { console.log(result); } }); } }; + + $scope.inlineChangeFKTarget = function(idx) { + // This function notes a change in the target of the target of a foreign key + // If there is already a target, we should delete that FK and create a new one + // This is meant to be transitional until we add an FK modify function to the API + // If there was not a target, we should create a new FK + if ($scope.table.fields && $scope.table.fields[idx]) { + var field = $scope.table.fields[idx]; + var new_target_id = field.target_id; + + var fks = Metabase.field_foreignkeys({'fieldId': field.id}); + + if(fks.length > 0){ + // delete this key + var relationship_id = 0; + ForeignKey.delete({'fkID': fks[0].id}, function(result){ + console.log("Deleted FK"); + Metabase.field_addfk({"db": field.table.db.id, "fieldId":field.id,'target_field': new_target_id, "relationship": "Mt1"}); + + }, function(error){ + console.log('Error deleting key ', error); + }); + }else{ + + Metabase.field_addfk({"db": field.table.db.id, "fieldId":field.id,'target_field': new_target_id, "relationship": "Mt1"}); + } + } + }; + + $scope.deleteTarget = function(field, target){ + + }; }]); diff --git a/resources/frontend_client/app/admin/datasets/partials/dataset_edit.html b/resources/frontend_client/app/admin/datasets/partials/dataset_edit.html index 31f73e6dd3968da1e30e3e93f9194712b2d06585..5a9efe2f718b8a1d42210c4c2104be96f872bb3d 100644 --- a/resources/frontend_client/app/admin/datasets/partials/dataset_edit.html +++ b/resources/frontend_client/app/admin/datasets/partials/dataset_edit.html @@ -27,7 +27,6 @@ <select ng-class="{CustomTypeApplied: table.entity_type }" ng-model="table.entity_type" ng-change="inlineSave()" ng-options="ent_type.id as ent_type.name for ent_type in utils.table_entity_types"> </select> </label> - <div class="text-grey-4 mt1"> <a e-class="full" href="#" ng-class="{EditedEntity: table.description }" editable-text="table.description" onaftersave="inlineSave()"> {{table.description}} @@ -42,7 +41,7 @@ <div class="DragBoundary mt2 border-bottom"> <ul as-sortable="dragControlListeners" ng-model="fields"> - <li class="border-top py2 relative text-grey-3" ng-repeat="field in fields" as-sortable-item> + <li class="border-top py2 relative text-grey-3" ng-repeat="field in table.fields" as-sortable-item> <div class="Drag-handle" title="Reorder" as-sortable-item-handle> <svg width="9" height="36"> <rect class="Dragger" fill="url(#dragger)" width="6" height="42"> @@ -63,9 +62,15 @@ </select> </label> <label class="Select mx1"> - <select ng-class="{CustomTypeApplied: field.special_type }" ng-model="field.special_type" ng-change="inlineSaveField($index)" ng-options="spec_type.id as spec_type.name for spec_type in utils.field_special_types"> + <select ng-class="{CustomTypeApplied: field.special_type }" ng-model="field.special_type" ng-change="inlineSpecialTypeChange($index)" ng-options="spec_type.id as spec_type.name for spec_type in utils.field_special_types"> + </select> + </label> + <label class="Select mx1" ng-if="field.special_type=='fk'"> + <select ng-model="field.target_id" ng-change="inlineChangeFKTarget($index)" ng-options="idf.id as idf.displayName for idf in idfields"> + <option value="" disabled selected>Target Field</option> </select> </label> + </div> <div class="pt1 full"> <a e-class="full" href="#" ng-class="{EditedEntity: field.description }" editable-text="field.description" onaftersave="inlineSaveField($index)"> diff --git a/resources/frontend_client/app/admin/datasets/partials/dataset_list.html b/resources/frontend_client/app/admin/datasets/partials/dataset_list.html index b03f17dd0c4ed833068a12f49695feac67d5eed7..dd1f4719a2efabfda7e1f835323d7941d1b5cf5c 100644 --- a/resources/frontend_client/app/admin/datasets/partials/dataset_list.html +++ b/resources/frontend_client/app/admin/datasets/partials/dataset_list.html @@ -1,15 +1,19 @@ <div class="wrapper"> <div ng-repeat="db in databases"> - <h2>{{db.name}}</h2> + <div class="py2 border-bottom"> + <div class="float-right"> + <a class="Button Button--primary" ng-click="syncDatabase(db.id)">Re-Sync Database</a> + </div> + <h2>{{db.name}}</h2> + + </div> <ul> - <li class="py1" ng-repeat="table in db.tables"> - <a class="EntityListItem" ng-class="{ EditedEntity: table.entity_name, EditedEntityMarker: table.entity_name }" cv-org-href="/admin/datasets/{{table.id}}"> + <li class="py2 border-bottom" ng-repeat="table in db.tables"> + <h4><a class="EntityListItem" ng-class="{ EditedEntity: table.entity_name, EditedEntityMarker: table.entity_name }" cv-org-href="/admin/datasets/{{table.id}}"> <span class="ModifiedEntity">{{table.entity_name}}</span> <span ng-if="!table.entity_name">{{table.name}}</span> </a> - </li> - <li> - <a class="Button" ng-click="syncDatabase(db.id)">Re-Sync Database</a> + </h4> </li> </ul> </div> diff --git a/resources/frontend_client/app/admin/datasets/partials/field_detail.html b/resources/frontend_client/app/admin/datasets/partials/field_detail.html index b43cb585d1de9fa2b7cbcf4b7c6449598f0b5093..7b9df7ab31d323fbdbc361fc739aff029b3ff462 100644 --- a/resources/frontend_client/app/admin/datasets/partials/field_detail.html +++ b/resources/frontend_client/app/admin/datasets/partials/field_detail.html @@ -40,8 +40,6 @@ </tbody> </table> </div> - <br /> - <input type="button" value="Add Relationship" class="Button" role="button" field="field" callback="relationshipAdded" cv-field-relationship-modal /> <hr /> <cv-comments type="field" id="field.id" title="Notes"></cv-comments> </div> diff --git a/resources/frontend_client/app/card/card.charting.js b/resources/frontend_client/app/card/card.charting.js index ca2357bbd0f7ab26574ec3f82985cc0b0a8ca312..6d7969ba2d87dc6b90be89779f913124835b11ff 100644 --- a/resources/frontend_client/app/card/card.charting.js +++ b/resources/frontend_client/app/card/card.charting.js @@ -119,6 +119,7 @@ function getComputedProperty(prop, elementOrId) { // computed size properties (drop 'px' and convert string -> Number) function getComputedSizeProperty(prop, elementOrId) { var val = getComputedProperty(prop, elementOrId); + if (!val) return null; return Number(val.replace("px", "")); } var getComputedWidth = _.partial(getComputedSizeProperty, "width"); diff --git a/resources/frontend_client/app/card/card.controllers.js b/resources/frontend_client/app/card/card.controllers.js index 21151bdff4e023dc60252928fb3697d5ed804f57..81dea312627ab886b98df9a27d7ec1c01c265aaa 100644 --- a/resources/frontend_client/app/card/card.controllers.js +++ b/resources/frontend_client/app/card/card.controllers.js @@ -1450,6 +1450,12 @@ CardControllers.controller('CardDetailNew', [ console.log('could not run card!', error); }); }, + setDisplay: function (type) { + // change the card visualization type and refresh chart settings + $scope.model.card.display = type; + $scope.model.card.visualization_settings = VisualizationSettings.getSettingsForVisualization({}, type); + $scope.model.inform(); + }, }; if ($routeParams.cardId) { // loading up an existing card diff --git a/resources/frontend_client/app/controllers.js b/resources/frontend_client/app/controllers.js index d83929cb0db7bd324ab16f74879745876ea9043b..2dccaf5a961a530901fd30b8b236759bd998fcb9 100644 --- a/resources/frontend_client/app/controllers.js +++ b/resources/frontend_client/app/controllers.js @@ -22,6 +22,8 @@ CorvusControllers.controller('Corvus', ['$scope', '$location', 'CorvusCore', 'Co // current User // TODO: can we directly bind to Appstate.model? $scope.user = undefined; + $scope.userMemberOf = undefined; + $scope.userAdminOf = undefined; $scope.userIsAdmin = false; // current Organization @@ -34,13 +36,15 @@ CorvusControllers.controller('Corvus', ['$scope', '$location', 'CorvusCore', 'Co $scope.$on("appstate:user", function (event, user) { // change in current user $scope.user = user; + $scope.userMemberOf = user.memberOf(); + $scope.userAdminOf = user.adminOf(); }); $scope.$on("appstate:organization", function (event, org) { // change in current organization $scope.currentOrgSlug = org.slug; $scope.currentOrg = org; - $scope.userIsAdmin = AppState.userIsAdmin(); + $scope.userIsAdmin = $scope.user.isAdmin(org.slug); }); $scope.$on("appstate:logout", function (event, user) { @@ -66,14 +70,6 @@ CorvusControllers.controller('Corvus', ['$scope', '$location', 'CorvusCore', 'Co $scope.refreshCurrentUser = function() { AppState.refreshCurrentUser(); }; - - $scope.memberOf = function(){ - return AppState.memberOf(); - }; - - $scope.adminOf = function(){ - return AppState.adminOf(); - }; }]); diff --git a/resources/frontend_client/app/css/query_builder.css b/resources/frontend_client/app/css/query_builder.css index 96e98571a0e46e29094ccd3c87d710ca1f7015af..f1ea1d882ba24f56f6ed4f90e5ac9855fbc2ad38 100644 --- a/resources/frontend_client/app/css/query_builder.css +++ b/resources/frontend_client/app/css/query_builder.css @@ -15,11 +15,23 @@ body { background: #fff; border-radius: 4px; position: absolute; - bottom: 2em; - right: 2em; + bottom: 0.5em; + right: 1.5em; width: 400px; } +.QueryBuilder .dc-chart { + padding: 2em; +} + +.QueryBuilder .dc-chart .pie-slice { + font-size: 1em; +} + +.QueryWrapper { + padding-left: 1.5em; + padding-right: 1.5em; +} .SaveModal.Modal--showing { border: 1px solid #ddd; @@ -50,11 +62,20 @@ body { display: block; } +.SaveButton { + position: relative; + z-index: 10; +} + +.QueryBar { + display: inline-block; + margin: 0 0 1.2em 0; +} + .QueryName { font-weight: 200; - margin-right: 1em; - font-size: 1.5em; - margin: 2em 0 1.25em; + margin: 1em 0; + font-size: 1.35em; } .QueryBar .SelectionModule { @@ -62,11 +83,15 @@ body { display: inline-block; } +.SelectionModule { + color: var(--brand-color); +} + .SelectionModule .SelectionItems { opacity: 0; pointer-events: none; position: absolute; - top: 2em; + top: 2.75em; border: 1px solid #ddd; box-shadow: 0 1px 4px rgba(0, 0, 0, .18); border-radius: 4px; @@ -74,13 +99,12 @@ body { z-index: 2; left: 0; right: 0; - overflow: hidden; + overflow-y: scroll; max-height: 360px; } .SelectionList { max-height: 420px; - overflow-y: scroll; } .SelectionItems.open { @@ -89,12 +113,13 @@ body { } .SelectionItem { - padding: 0.8em; - font-size: 1em; + padding: 0.75em; + font-size: 0.85em; position: relative; background: #fff; display: flex; align-items: center; + cursor: pointer; } .SelectionItems { @@ -102,10 +127,14 @@ body { } .SelectionItem:hover { - background: #FBFCFD; - transition: background .3s linear; + background-color: currentColor; +} + +.SelectionItem:hover .SelectionModule-display { + color: #fff; } + .SelectionItem:before { content: ''; width: 16px; @@ -113,6 +142,7 @@ body { border: 2px solid #ddd; border-radius: 99px; margin-right: 1em; + color: currentColor; } .SelectionItem.selected { @@ -121,24 +151,16 @@ body { } .SelectionItem.selected:before { - color: inherit; background-color: currentColor; border-color: currentColor; } .QueryBar .SelectionItem:hover:before { + color: #fff; border-color: currentColor; transition: border .3s linear; } -.SelectionTitle { - display: block; -} - -.SelectionTitle:hover { - cursor: pointer; -} - .QueryBar .SelectionTitle { text-align: left; font-size: 0.85em; @@ -146,6 +168,26 @@ body { border-radius: 2px; } +.FilterSection .SelectionModule, +.FilterSection .SelectionModule a { + color: #A8C28e; +} + +.FilterSection .input { + color: #A8C28e; + border-color: transparent; +} + +.FilterSection .input:focus { + border-color: transparent; + border-bottom: 1px solid #A8C28e; +} + +.DimensionList .SelectionModule, +.DimensionList .SelectionModule a { + color: #B9A2CD; +} + .QueryBar .ActionButton { padding: 0.75em 1.25em; border: 1px dotted #ddd; @@ -173,7 +215,7 @@ body { } .SelectionModule-display { - color: #444; + color: currentColor; } .SelectionModule.selected .SelectionTitle { @@ -184,6 +226,11 @@ body { box-shadow: 0 1px 1px rgba(0, 0, 0, .08); } +.SelectionModule.selected .SelectionTitle:hover { + box-shadow: 0 1px 1px rgba(0, 0, 0, .18); + cursor: pointer; +} + .RemoveTrigger { display: inline-block; @@ -192,9 +239,9 @@ body { .ActionBar { position: fixed; bottom: 0; - left: 0; right: 0; padding: 1em; + z-index: 100; } .ActionButton { @@ -283,35 +330,10 @@ body { } .QueryPicker-group { - padding: 2em 0; border-bottom: 1px solid #ddd; background: #fff; } - -/* -.with-breakouts:after { - content: 'By'; - display: block; - position: absolute; - top: 0; - right: -1.5em; - bottom: 0; - color: #999; - border: 1px solid #ddd; - border-radius: 99px; - background: #fff; - width: 3em; - line-height: 3em; - box-shadow: 0 1px 3px rgba(0, 0, 0, .12); -} -*/ - -.SaveButton { - position: relative; - z-index: 5; -} - .QueryBar .ActionButton { font-size: 0.85em; } @@ -377,6 +399,8 @@ body { .Metric-sourceTable:after { content: '•'; display: inline-block; + color: #ddd; + margin-left: 1em; } .DimensionList .RemoveTrigger { @@ -391,9 +415,6 @@ body { .SelectionTitle { padding: 0.75em 1.25em; } - .QueryPicker-group { - padding: 1.2em; - } } @media screen and (min-width: 120em) { @@ -441,25 +462,6 @@ body { } -.FilterSection .SelectionModule, -.FilterSection .SelectionModule a { - color: #A8C28e; -} - -.FilterSection .input { - color: #A8C28e; - border-color: transparent; -} - -.FilterSection .input:focus { - border-color: transparent; - border-bottom: 1px solid #A8C28e; -} - -.DimensionList .SelectionModule, -.DimensionList .SelectionModule a { - color: #B9A2CD; -} .FilterTrigger .icon { display: flex; @@ -469,3 +471,20 @@ body { overflow: hidden; background: #fff; } + +.TypeControls { + position: fixed; + bottom: 0; + width: 100%; + z-index: 100; + left: 0; + right: 0; + text-align: center; + margin: 1em 0; +} + +.TypeControls .ActionButton { + display: inline-block; + font-size: 0.85em; + margin: 0 0.5em; +} diff --git a/resources/frontend_client/app/react/query_builder.js b/resources/frontend_client/app/react/query_builder.js index 2e94c411ff2c0232b83defcb55542dd85141c136..31bb388b56dc78c33862d775cd115151f7f09c78 100644 --- a/resources/frontend_client/app/react/query_builder.js +++ b/resources/frontend_client/app/react/query_builder.js @@ -4,6 +4,122 @@ var cx = React.addons.classSet, ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; +var QueryVisualization = React.createClass({ + getInitialState: function () { + return { + type: 'table', + chartId: Math.floor((Math.random() * 698754) + 1) + } + }, + _changeType: function (type) { + this.setState({ + type: type + }); + this.props.setDisplay(type); + }, + componentDidMount: function () { + if (this.state.type !== 'table') { + CardRenderer[this.state.type](this.state.chartId, this.props.card, this.props.result.data); + } + }, + componentDidUpdate: function () { + if (this.state.type !== 'table') { + CardRenderer[this.state.type](this.state.chartId, this.props.card, this.props.result.data); + } + }, + render: function () { + // for table rendering + var tableRows, + tableHeaders, + table; + // for chart rendering + var titleId, + innerId; + + if(this.props.result && this.props.result.data) { + if(this.state.type === 'table') { + tableRows = this.props.result.data.rows.map(function (row) { + var rowCols = row.map(function (data) { + return (<td>{data.toString()}</td>) + }); + + return (<tr>{rowCols}</tr>); + }); + + tableHeaders = this.props.result.data.columns.map(function (column) { + return ( + <th>{column.toString()}</th> + ); + }); + + table = ( + <table className="QueryTable Table"> + <thead> + <tr> + {tableHeaders} + </tr> + </thead> + <tbody> + {tableRows} + </tbody> + </table> + ); + } else { + titleId = 'card-title--'+this.state.chartId; + innerId = 'card-inner--'+this.state.chartId; + } + } + + var viz; + if(this.state.type != 'table') { + viz = ( + <div class="Card--{this.state.type} Card-outer px1" id={this.state.chartId}> + <div id={titleId} class="text-centered"></div> + <div id={innerId} class="card-inner"></div> + </div> + ); + } else { + viz = ( + <div className="Table-wrapper"> + {table} + </div> + ); + } + + var types = [ + 'table', + 'line', + 'bar', + 'pie', + 'area', + 'timeseries' + ], typeControls = types.map(function (type) { + if(this.props.result) { + var buttonClasses = cx({ + 'ActionButton': true, + 'ActionButton--primary' : (type == this.state.type) + }) + return ( + <a className={buttonClasses} href="#" onClick={this._changeType.bind(null, type)}>{type}</a> + ); + } else { + return false; + } + }.bind(this)); + + return ( + <div> + <div className="TypeControls"> + <div className="wrapper"> + {typeControls} + </div> + </div> + {viz} + </div> + ); + } +}); + var ResultTable = React.createClass({ render: function () { var table, @@ -109,7 +225,7 @@ var Saver = React.createClass({ <div className="ModalContent"> <input ref="name" type="text" placeholder="Name" autofocus defaultValue={this.props.name} /> <input ref="description" type="text" placeholder="Add a description" defaultValue={this.props.description}/> - <div className="mt4 clearfix"> + <div className="mt4 ml2 mr2 clearfix"> <span className="text-grey-3 inline-block my1">Privacy:</span> <div className="float-right"> <SelectionModule @@ -388,7 +504,7 @@ var QueryPicker = React.createClass({ } aggregationTargetHtml = ( <SelectionModule - placeholder='Lets start with...' + placeholder='field named...' items={this.props.aggregationFieldList[0]} display='1' selectedValue={this.props.query.aggregation[1]} @@ -497,10 +613,19 @@ var FilterWidget = React.createClass({ } if(this.props.valueFields) { if(this.props.valueFields.values) { + // do some fixing up of the values so we can display true / false safely + var values = this.props.valueFields.values.map(function(value) { + var safeValues = {} + for(var key in value) { + safeValues[key] = value[key].toString() + } + return safeValues + }) + valueHtml = ( <SelectionModule placeholder="..." - items={this.props.valueFields.values} + items={values} display='name' selectedValue={this.props.value} selectedKey='key' @@ -522,7 +647,7 @@ var FilterWidget = React.createClass({ <div className="QueryFilter relative inline-block"> <div className="FilterSection"> <SelectionModule - placeholder="..." + placeholder="Filter by..." items={this.props.filterFieldList} display='name' selectedValue={this.props.field} @@ -647,11 +772,22 @@ var QueryBuilder = React.createClass({ 'QueryPicker-group': true }) + var saver + if(this.props.model.result) { + saver = ( + <Saver + save={this.props.model.save.bind(this.props.model)} + name={this.props.model.card.name} + description={this.props.model.card.description} + hasChanged={this.props.model.hasChanged} + /> + ) + } return ( <div className="full-height"> <div className="QueryHeader"> - <div className="wrapper"> + <div className="QueryWrapper"> <QueryHeader name={this.props.model.card.name} user={this.props.model.user} @@ -660,7 +796,7 @@ var QueryBuilder = React.createClass({ </div> <div className={queryPickerClasses}> <div> - <div className="wrapper"> + <div className="QueryWrapper"> <div className="clearfix"> {runButton} <QueryPicker @@ -683,24 +819,22 @@ var QueryBuilder = React.createClass({ </div> </div> <div> - <div className="wrapper my2"> + <div className="QueryWrapper my2"> {filterHtml} </div> </div> </div> - - <div className="wrapper"> - <ResultTable result={this.props.model.result} /> + <div className="QueryWrapper mb4"> + <QueryVisualization + card={this.props.model.card} + result={this.props.model.result} + setDisplay={this.props.model.setDisplay.bind(this.props.model)} + /> </div> <div className="ActionBar"> - <Saver - save={this.props.model.save.bind(this.props.model)} - name={this.props.model.card.name} - description={this.props.model.card.description} - hasChanged={this.props.model.hasChanged} - /> + {saver} </div> </div> ) diff --git a/resources/frontend_client/app/services.js b/resources/frontend_client/app/services.js index cd48d8a14817f0e5bf935d452de3054fa11d7119..2194f234dd7b35459a7243042ba2826d49cad39a 100644 --- a/resources/frontend_client/app/services.js +++ b/resources/frontend_client/app/services.js @@ -51,6 +51,36 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati User.current(function(result) { service.model.currentUser = result; + // add isMember(orgSlug) method to the object + service.model.currentUser.isMember = function (orgSlug) { + return this.org_perms.some(function (org_perm) { + return org_perm.organization.slug === orgSlug; + }); + }; + + // add isAdmin(orgSlug) method to the object + service.model.currentUser.isAdmin = function (orgSlug) { + return this.org_perms.some(function (org_perm) { + return org_perm.organization.slug === orgSlug && org_perm.admin; + }) || this.is_superuser; + }; + + // add memberOf() method to the object enumerating Organizations user is member of + service.model.currentUser.memberOf = function () { + return this.org_perms.map(function (org_perm) { + return org_perm.organization; + }); + }; + + // add adminOf() method to the object enumerating Organizations user is admin of + service.model.currentUser.adminOf = function () { + return this.org_perms.filter(function (org_perm) { + return org_perm.admin; + }).map(function (org_perm) { + return org_perm.organization; + }); + }; + $rootScope.$broadcast('appstate:user', result); deferred.resolve(result); @@ -62,33 +92,6 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati return deferred.promise; }, - userIsAdmin: function() { - // Let's also figure out if this user is an admin each time the user or the organization changes - return service.model.currentUser.org_perms.some(function(org_perm) { - return org_perm.organization.slug === $routeParams.orgSlug && org_perm.admin; - }); - }, - - userIsMember: function() { - return service.model.currentUser.org_perms.some(function(org_perm) { - return org_perm.organization.slug === $routeParams.orgSlug; - }); - }, - - memberOf: function() { - return service.model.currentUser.org_perms.map(function(org_perm) { - return org_perm.organization; - }); - }, - - adminOf: function() { - return service.model.currentUser.org_perms.filter(function(org_perm) { - return org_perm.admin; - }).map(function(org_perm) { - return org_perm.organization; - }); - }, - // This function performs whatever state cleanup and next steps are required when a user tries to access // something they are not allowed to. invalidAccess: function(user, url, message) { @@ -153,8 +156,8 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati // PERMISSIONS CHECK!! user must be member of this org to proceed // Making convenience vars so it's easier to scan conditions for correctness var isSuperuser = service.model.currentUser.is_superuser; - var isOrgMember = service.userIsMember(); - var isOrgAdmin = service.userIsAdmin(); + var isOrgMember = service.model.currentUser.isMember($routeParams.orgSlug); + var isOrgAdmin = service.model.currentUser.isAdmin($routeParams.orgSlug); var onAdminPage = $location.path().indexOf('/' + $routeParams.orgSlug + '/admin') === 0; if (!isSuperuser && !isOrgMember) { diff --git a/resources/frontend_client/index.html b/resources/frontend_client/index.html index da06a74353e372706dcf794db3db408abace874b..4934545bc204ce90321b9f62e7ad749dcc27df3a 100644 --- a/resources/frontend_client/index.html +++ b/resources/frontend_client/index.html @@ -23,7 +23,7 @@ <cv-chevron-down-icon width="8px" height="8px"></cv-chevron-down-icon> </span> <ul class="Dropdown-content"> - <li ng-repeat="organization in memberOf()"> + <li ng-repeat="organization in userMemberOf"> <a class="link block py1" href="#" ng-click="changeCurrOrg(organization.slug)">{{organization.name}}</a> </li> </ul> @@ -83,7 +83,7 @@ <div class="border-bottom bg-white py1"> <div class="wrapper"> <label class="Select" ng-if="user.is_multi_org"> - <select ng-change="changeCurrOrg(currentOrgSlug)" ng-model="currentOrgSlug" ng-options="organization.slug as organization.name for organization in adminOf()"></select> + <select ng-change="changeCurrOrg(currentOrgSlug)" ng-model="currentOrgSlug" ng-options="organization.slug as organization.name for organization in userAdminOf"></select> </select> </label> <a cv-org-href="/admin/">