How can I dynamically change the visibility of a Durandal Route? - durandal

I have my Durandal routes configured as below.
var routes = [
....... More Routes Here.....
{
url: 'login',
moduleId: 'viewmodels/login',
name: 'Log In',
visible: true,
caption: 'Log In'
}, {
url: 'logout',
moduleId: 'viewmodels/logout',
name: 'Log Out',
visible: false,
caption: 'Log Out'
}, {
url: 'register',
moduleId: 'viewmodels/register',
name: 'Register',
visible: false,
caption: 'Register'
}];
And everything is working as expected. I would like to be able to activate the Logout Route in my navigation when I log in and my log in button to become invisible. I have tried the following code and despite not throwing any errors it does not change the visibility of anything in the interface.
var isLoggedIn = ko.observable(false);
isLoggedIn.subscribe(function (newValue) {
var routes = router.allRoutes();
if (newValue == true) {
for (var k = 0; k < routes.length; k++) {
if (routes[k].url == 'logout') {
routes[k].visible = true;
}
if (routes[k].url == 'login') {
routes[k].visible = false;
}
}
} else {
for (var i = 0; i < routes.length; i++) {
if (routes[i].url == 'logout') {
routes[i].visible = false;
}
if (routes[i].url == 'login') {
routes[i].visible = true;
}
}
}
});
I believe this doesn't work because visible is not an observable, isActive is a computed with no write capability so it does not work either. How can I dynamically change the visibility of my routes in the nav menu?

Here is what I ended up doing.
//ajax call to log user in
.done(function (recievedData) {
if (recievedData == true) {
router.deactivate();
return router.map(config.routesLoggedIn);
} else {
router.deactivate();
return router.map(config.routes);
}
}).done(function() {
router.activate('frames');
return router.navigateTo('#/frames');
});
Essentially created two routing profiles in my config object. One for logged in and one for not. There is one caveat. The router.deactivate() method is a very new method and is not in the NuGet package yet. I copied the code of the new router from the master branch of the GitHub repository for Durandal. There is some discussion on this new function on the Durandal User Group. Ultimately for security reasons I might feed the logged in routes from my server. But for the time being this should work just fine.

Another approach is to make all routes available but use bound expressions to compose either the content or the login page into the container view specified by the route.
Instead of supplying a literal view name, bind the compose parameter to a ternary expression that chooses between the name of the login view and the name of the content view. The controlling expression would be an observable such as app.isAuthenticated() the value of which must be set when the user succeeds in logging in or out.
This approach is robust in the face of deep linking because it does away with the notion of a path through the application. Without explicit redirection logic, it will authenticate the user and then show the requested resource.
It can be extended to more than two possible states using a function instead of a ternary expression. This is handy when different UI must be delivered according to user permission.

Related

Youtrack check if user has permissions

I'm trying to create a Youtrack workflow where only a specific role is allowed to edit the Kanban state to ready-to-pull when the current issue is in backlog. I wasn't quite able to get it correctly working, keeps throwing exceptions but I'm unable to read the full exception.
I tried to create the current workflow code:
var entities = require('#jetbrains/youtrack-scripting-api/entities');
var workflow = require('#jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: workflow.i18n('Block change in Kanban stage for issues that are in backlog'),
guard: function(ctx) {
return ctx.issue.isReported && ctx.issue.fields.isChanged(ctx.KanbanState);
},
action: function(ctx) {
var issue = ctx.issue;
if (!ctx.user.hasRole('project-admin', ctx.project)) {
workflow.message('U dont have the correct permissions to do this');
ctx.KanbanState = ctx.KanbanState.Blocked;
}
},
requirements: {
Stage: {
type: entities.State.fieldType
},
KanbanState: {
name: 'Kanban State',
type: entities.EnumField.fieldType,
ReadyToPull: {
name: 'Ready to pull'
},
Blocked: {}
}
}
});
Most of this is a copy from the Kanban change workflow that blocks moving the issue to a new stage when the kanban state isn't set to "Ready-to-Pull". I basically want the exact same, but I want to only allow project admins to change the kanban state to "ready-to-pull" when the current stage is "Backlog". The current code only checks the permissions at the moment, but I started getting stuck there already.
To implement this task, I suggest you use the workflow.check method, for example:
workflow.check(ctx.user.hasRole('project-admin', ctx.project), 'U dont have the correct permissions to do this');
I hope this helps.
Seeing as in our current situation we only need to disable a single person to not be able to change the Kanban states when new tickets are set, we have the following solution:
exports.rule = entities.Issue.onChange({
title: workflow.i18n('Block change in Kanban stage for issues in backlog stage'),
guard: function(ctx) {
var issue = ctx.issue;
return issue.fields.isChanged(ctx.KanbanState);//Runs when Kanban state changes
},
action: function(ctx) {
var issue = ctx.issue;
//Check if user has changed the kanban state to ready to pull while the current stage is backlog.
if (issue.fields.Stage.name == 'Backlog') {
//Current stage is backlog
if (issue.fields.KanbanState.name === ctx.KanbanState.ReadyToPull.name) {
//Kanban state was changed to ready to pull;
var target = '<useremail>';
workflow.check(ctx.currentUser.email == target,
workflow.i18n('No permissions to change the Kanban state.'));
issue.fields.KanbanState = ctx.KanbanState.Blocked;
}
}
},
requirements: {
Stage: {
type: entities.State.fieldType,
Backlog: {},
Development: {}
},
KanbanState: {
name: 'Kanban State',
type: entities.EnumField.fieldType,
ReadyToPull: {
name: 'Ready to pull'
},
Blocked: {}
}
}
});
However I checked the answer of #Oleg Larshun and his answer worked as well. replacing the email section with:
workflow.check(ctx.user.hasRole('project-admin', ctx.project), 'U dont have the correct permissions to do this');

Firebase make user object from auth data

So I'm using Angularfire in an ionic app and trying to figure out how to make a user object that is associated with the auth data from an Auth $createUser call. My first try had the auth call and the user got authenticated, then a user object was made and pushed into a $firebaseArray which works fine, but I don't know how to grab the current user after they are logged in to update, destory, or do anything with that users data. I have made it work with looping through the users array and matching the uid from the user array item and the auth.uid item which was set to be the same in the user array object creation. This seems really ineffecient to loop over if there is a large user array and it needs to be done on multiple pages.
My current attempt is using a different method like so:
angular.module('haulya.main')
.controller('RegisterController', ['Auth', '$scope', 'User', '$ionicPlatform', '$cordovaCamera','CurrentUserService',
function(Auth, $scope, User, $ionicPlatform, $cordovaCamera, CurrentUserService) {
//scope variable for controller
$scope.user = {};
console.log(User);
$scope.createUser = function(isValid) {
var userModel;
$scope.submitted = true;
//messages for successful or failed user creation
$scope.user.message = null;
$scope.user.error = null;
//if form is filled out valid
if(isValid) {
//Create user with email and password firebase Auth method
Auth.$createUser({
email: $scope.user.email,
password: $scope.user.password
})
.then(function(userData) {
userModel = {
uid: userData.uid,
photo: $scope.user.photo || null,
firstName: $scope.user.firstName,
lastName: $scope.user.lastName,
email: $scope.user.email,
cell: $scope.user.cell,
dob: $scope.user.dob.toString(),
city: $scope.user.city,
state: $scope.user.state,
zip: $scope.user.zip
}
// add new user to profiles array
User.create(userModel).then(function(user) {
$scope.sharedUser = User.get(user.path.o[1]);
});
$scope.user.message = "User created for email: " + $scope.user.email;
})
.catch(function(error) {
//set error messages contextually
if(error.code == 'INVALID_EMAIL') {
$scope.user.error = "Invalid Email";
}
else if(error.code == 'EMAIL_TAKEN'){
$scope.user.error = "Email already in use, if you think this is an error contact an administrator";
}
else {
$scope.user.error = "Fill in all required fields";
}
});
}
};
//Get profile pic from camera, or photo library
$scope.getPhoto = function(type) {
$ionicPlatform.ready(function() {
//options for images quality/type/size/dimensions
var options = {
quality: 65,
destinationType: Camera.DestinationType.DATA_URL,
sourceType: Camera.PictureSourceType[type.toUpperCase()],
allowEdit: true,
encodingType: Camera.EncodingType.JPEG,
targetWidth: 100,
targetHeight: 100,
popoverOptions: CameraPopoverOptions,
saveToPhotoAlbum: false
};
//get image function using cordova-plugin-camera
$cordovaCamera.getPicture(options)
.then(function(photo) {
$scope.user.photo = photo;
}, function(err) {
console.log(err);
});
});
};
}]);
And here's the service the controller is using:
angular
.module('haulya.main')
.factory('User', function($firebaseArray) {
var ref = new Firebase('https://haulya.firebaseio.com');
var users = $firebaseArray(ref.child('profiles'));
var User = {
all: users,
create: function(user) {
return users.$add(user);
},
get: function(userId) {
return $firebaseArray(ref.child('profiles').child(userId));
},
delete: function(user) {
return users.$remove(user);
}
};
return User;
});
This also works, but again I don't have a solid reference to the currently logged in users object data from the array. The objects id is only stored on the controllers scope.
I looked through other posts, but they were all using older versions of firebase with deprecated methods.
If you're storing items that have a "natural key", it is best to store them under that key. For users this would be the uid.
So instead of storing them with $add(), store them with child().set().
create: function(user) {
var userRef = users.$ref().child(user.uid);
userRef.set(user);
return $firebaseObject(userRef);
}
You'll note that I'm using non-AngularFire methods child() and set(). AngularFire is built on top of Firebase's regular JavaScript SDK, so they interoperate nicely. The advantage of this is that you can use all the power of the Firebase JavaScript SDK and only use AngularFire for what it's best at: binding things to Angular's $scope.
Storing user data is explained in Firebase's guide for JavaScript. We store them under their uid there too instead of using push(), which is what $add() calls behind the scenes.

Why won't my MithrilJS UI render unless I call redraw after a programmatic redirect?

Here is a snippet of view code. Why won't it work without the m.redraw()? If I don't call that, the route changes and the login controller loads, but nothing is rendered into the DOM.
home.view = function(ctrl) {
console.log('in home view');
if (!mo_portal.logged_in) {
console.log('redirecting to login');
m.route("/login");
m.redraw();
return;
}
return m("div","HOME");
}
Changing route will always trigger a redraw. If you're not seeing the login page view without manually calling m.redraw it's probably down to bugs in the login controller or view that happen during the route change redraw - bugs whose failure conditions are reset when you call m.redraw again.
Here's an extension of your code with a login view and controller. mo_portal.logged_in is set to true or false depending on whether the user is one in the usersList or not, so we can test success and failure.
I took out the m.redraw (I also put the redirect logic in the home controller) and everything works fine.
var usersList = [
'john',
'alexia'
];
var mo_portal = {
username : '',
logged_in: false
};
var login = {};
login.controller = function(){
this.username = function( input ){
if( arguments.length ){
mo_portal.username = input;
mo_portal.logged_in = !!~usersList.indexOf( input );
}
return mo_portal.username;
};
};
login.view = function(ctrl){
console.log('in login view');
return [
m( 'input', { oninput : m.withAttr( 'value', ctrl.username ), value : ctrl.username() } ),
m( 'a[href=/home]', { config : m.route }, 'Login' )
];
};
var home = {};
home.controller = function(){
if (!mo_portal.logged_in) {
console.log('redirecting to login');
m.route("/login");
}
};
home.view = function(ctrl) {
console.log('in home view');
return m("div","HOME");
};
m.route( document.body, '/login', {
'/login' : login,
'/home' : home
} );
<script src="https://rawgit.com/lhorie/mithril.js/next/mithril.js"></script>
I think that's not how mithril is going to be used. Mithril does not expect a view update (aka redraw) during ongoing view construction.
I suppose you should do the route-change in the related controller.
Keep in mind, that a view is rendered evertime the page changes somehow. You probably don't want to check the login state evert time.

Building multiple navigation routes to the same module with DurandalJS

I would really like to take advantage of Durandal's buildNavigationModel() method and bind my UI nav to the router.navigationModel.
But in my case, I am essentially wanting three menu items, which all use the same underlying view and module, but vary only by parameter.
// . . .
activate: function () {
var standardRoutes = [
{ route: 'home', title: 'KPI Home', iconClass: "glyphicon-home", moduleId: 'viewmodels/kpihome', nav: true },
{ route: 'summary(/:category)', title: 'Quotes', iconClass: "glyphicon-home", moduleId: 'viewmodels/summary', hash: "#summary/quotes", nav: true },
{ route: 'summary(/:category)', title: 'Pricing', iconClass: "glyphicon-home", moduleId: 'viewmodels/summary', hash: "#summary/pricing", nav: true },
{ route: 'summary(/:category)', title: 'Sales', iconClass: "glyphicon-home", moduleId: 'viewmodels/summary', hash: "#summary/sales", nav: true }
];
router
.map(standardRoutes)
.buildNavigationModel();
return router.activate();
}
So while the hash is different, and I can pick up the category passed in to the summary module's activate method, when I click on either of the other routes, the first matching route isActive is flagging true. In other words, isActive works off the route pattern rather than an exact hash comparison.
Would anyone be able to recommend an alternate/best practice approach to this, where I could re-use route patterns and modules and still have a working nav?
My current solution would be to create the route only once and build my own nav model.
After a bit of digging I've found three potential solutions to this problem.
Use child routers
Use a custom identifier in the routes to describe child routes and parse these into the route table
Leave routing to the route table and create a custom navigation model
I'll chat through my opinion on each below and how I got to a solution. The following post really helped me a lot Durandal 2.0 - Child routers intended for nested menus?
Use child routers
While this makes a lot of sense when you want to create child routes that exist in a view served up by the main router, my requirement is to have navigation visible at a shell level that contains all sub routes, always visible and loaded.
According to the article mentioned above
"if we check the code of function creating child routes we will see that it creates new router and only store reference to parent router - the parent router ( in most cases main router) does not have references to its childs"
So I'm basing my decision on that (hopefully correct info) and for my case that looks like its not going to work.
Use a custom identifier in the routes to describe child routes and parse these into the route table
This works and is implemented neatly in the the article mentioned above. But does the route table have the same concerns as UI navigation? In some cases, sure it can share that, but in my case not, so I'm going with option 3, creating a custom nav model.
Creating a Custom Navigation Model
I'm needing to re-use some views for navigation items, to display the same summary and detail views, but for different categories and kpi's that I'll pass in by parameter.
From a route table perspective there are only three routes - the routes to a home view, a summary view and a detail view.
From a nav perspective there are n navigation items, depending on the number of categories and kpi's I want to display summary and detail views for. I'll effectively be putting links up for all the items I want to show.
So it makes sense that I build up the nav model independently of the route table.
utility\navigationModel.js
defines the navigation model and responds to hash changes to keep a record in the activeHash observable
define(["knockout", "utility/navigationItem"], function (ko, NavItem) {
var NavigationModel = function () {
this.navItems = ko.observableArray();
this.activeHash = ko.observable();
window.addEventListener("hashchange", this.onHashChange.bind(this), false);
this.onHashChange();
};
NavigationModel.prototype.generateItemUid = function () {
return "item" + (this.navItems().length + 1);
};
NavigationModel.prototype.onHashChange = function () {
this.activeHash(window.location.hash);
};
NavigationModel.prototype.findItem = function (uid) {
var i = 0,
currentNavItem,
findRecursive = function (uid, base) {
var match = undefined,
i = 0,
childItems = base.navItems && base.navItems();
if (base._uid && base._uid === uid) {
match = base;
} else {
for (; childItems && i < childItems.length; i = i + 1) {
match = findRecursive(uid, childItems[i]);
if (match) {
break;
}
}
}
return match;
};
return findRecursive(uid, this);
};
NavigationModel.prototype.addNavigationItem = function (navItem) {
var parent;
if (navItem.parentUid) {
parent = this.findItem(navItem.parentUid);
} else {
parent = this;
}
if (parent) {
parent.navItems.push(new NavItem(this, navItem));
}
return this;
};
return NavigationModel;
});
utility\navigationItem.js
represents a navigation item, with nav specific properties like iconClass, sub nav items navItems and a computed to determine if it is an active nav isActive
define(["knockout"], function (ko) {
var NavigationItem = function (model, navItem) {
this._parentModel = model;
this._uid = navItem.uid || model.generateItemUid();
this.hash = navItem.hash;
this.title = navItem.title;
this.iconClass = navItem.iconClass;
this.navItems = ko.observableArray();
this.isActive = ko.computed(function () {
return this._parentModel.activeHash() === this.hash;
}, this);
}
return NavigationItem;
});
shell.js
defines standard routes for the route table and builds up the custom navigation. Implemented properly this would likely call a dataservice to lookup categories and kpi's for the nav model
define([
'plugins/router',
'durandal/app',
'utility/navigationModel'
], function (router, app, NavigationModel) {
var customNavigationModel = new NavigationModel(),
activate = function () {
// note : routes are required for Durandal to function, but for hierarchical navigation it was
// easier to develop a custom navigation model than to use the Durandal router's buildNavigationModel() method
// so all routes below are "nav false".
var standardRoutes = [
{ route: '', moduleId: 'viewmodels/kpihome', nav: false },
{ route: 'summary(/:category)', moduleId: 'viewmodels/summary', hash: "#summary/quotes", nav: false },
{ route: 'kpidetails(/:kpiName)', moduleId: 'viewmodels/kpidetails', hash: "#kpidetails/quotedGMPercentage", nav: false }
];
router.map(standardRoutes);
// Fixed items can be added to the Nav Model
customNavigationModel
.addNavigationItem({ title: "KPI Home", hash: "", iconClass: "glyphicon-home" });
// items by category could be looked up in a database
customNavigationModel
.addNavigationItem({ uid: "quotes", title: "Quotes", hash: "#summary/quotes", iconClass: "glyphicon-home" })
.addNavigationItem({ uid: "sales", title: "Sales", hash: "#summary/sales", iconClass: "glyphicon-home" });
// and each category's measures/KPIs could also be looked up in a database and added
customNavigationModel
.addNavigationItem({ parentUid: "quotes", title: "1. Quoted Price", iconClass: "glyphicon-stats", hash: "#kpidetails/quotedPrice" })
.addNavigationItem({ parentUid: "quotes", title: "2. Quoted GM%", iconClass: "glyphicon-stats", hash: "#kpidetails/quotedGMPercentage" });
customNavigationModel
.addNavigationItem({ parentUid: "sales", title: "1. Quoted Win Rate", iconClass: "glyphicon-stats", hash: "#kpidetails/quoteWinRate" })
.addNavigationItem({ parentUid: "sales", title: "2. Tender Win Rate ", iconClass: "glyphicon-stats", hash: "#kpidetails/tenderWinRate" });
return router.activate();
};
return {
router: router,
activate: activate,
customNavigationModel: customNavigationModel
};
});
And thats it, a fair amount of code, but once in place it seperates the route table and the navigation model fairly nicely. All that remains is binding it to the UI, which I use a widget to do because it can serve as a recursive template.
widgets\verticalNav\view.html
<ul class="nav nav-pills nav-stacked" data-bind="css: { 'nav-submenu' : settings.isSubMenu }, foreach: settings.navItems">
<li data-bind="css: { active: isActive() }">
<a data-bind="attr: { href: hash }">
<span class="glyphicon" data-bind="css: iconClass"></span>
<span data-bind="html: title"></span>
</a>
<div data-bind="widget: {
kind: 'verticalNav',
navItems: navItems,
isSubMenu: true
}">
</div>
</li>
</ul>
I'm not suggesting this is the best way to do this, but if you want to seperate the concerns of a route table and a navigation model, its a potential solution :)

Durandal, get path of the current module

Is there a way in Durandal to get the path of the current module? I'm building a dashboard inside of a SPA and would like to organize my widgets in the same way that durandal does with "FolderWidgetName" and the folder would contain a controller.js and view.html file. I tried using the getView() method in my controller.js file but could never get it to look in the current folder for the view.
getView(){
return "view"; // looks in the "App" folder
return "./view"; // looks in the "App/durandal" folder
return "/view"; // looks in the root of the website
return "dashboard/widgets/htmlviewer/view" //don't want to hard code the path
}
I don't want to hardcode the path inside of the controller
I don't want to override the viewlocator because the rest of the app still functions as a regular durandal spa that uses standard conventions.
You could use define(['module'], function(module) { ... in order to get a hold on the current module. getView() would than allow you to set a specific view or, like in the example below, dynamically switch between multiple views.
define(['module'], function(module) {
var roles = ['default', 'role1', 'role2'];
var role = ko.observable('default');
var modulePath = module.id.substr(0, module.id.lastIndexOf('/') +1);
var getView = ko.computed(function(){
var roleViewMap = {
'default': modulePath + 'index.html',
role1: modulePath + 'role1.html',
role2: modulePath + 'role2.html'
};
this.role = (role() || 'default');
return roleViewMap[this.role];
});
return {
showCodeUrl: true,
roles: roles,
role: role,
getView: getView,
propertyOne: 'This is a databound property from the root context.',
propertyTwo: 'This property demonstrates that binding contexts flow through composed views.',
moduleJSON: ko.toJSON(module)
};
});
Here's a live example http://dfiddle.github.io/dFiddle-1.2/#/view-composition/getView
You can simply bind your setup view to router.activeRoute.name or .url and that should do what you are looking for. If you are trying to write back to the setup viewmodels property when loading you can do that like below.
If you are using the revealing module you need to define the functions and create a module definition list and return it. Example :
define(['durandal/plugins/router', 'view models/setup'],
function(router, setup) {
var myObservable = ko.observable();
function activate() {
setup.currentViewName = router.activeRoute.name;
return refreshData();
}
var refreshData = function () {
myDataService.getSomeData(myObservable);
};
var viewModel = {
activate: activate,
deactivate: deactivate,
canDeactivate: canDeactivate
};
return viewModel;
});
You can also reveal literals, observables and even functions directly while revealing them -
title: ko.observable(true),
desc: "hey!",
canDeactivate: function() { if (title) ? true : false,
Check out durandal's router page for more info on what is available. Also, heads up Durandal 2.0 is switching up the router.
http://durandaljs.com/documentation/Router/
Add an activate function to your viewmodel as follows:
define([],
function() {
var vm = {
//#region Initialization
activate: activate,
//#endregion
};
return vm;
//#region Internal methods
function activate(context) {
var moduleId = context.routeInfo.moduleId;
var hash = context.routeInfo.hash;
}
//#endregion
});