I hope someone can help me with this.
I have a Backbone based SPA for a responsive website with a .net WebAPI providing all of the data.
I've recently found a weird problem. I've added a search box, which searches one of the catalogues on the system. This works fine on desktop browsers and on Android. On iOS, executing a search seems to take you back to the sign in page.
You can execute a search in various ways, you can either hit enter or you can click the search icon. Both of these then trigger a method that navigates the router to the URL for the search result.
My first thought was that it was some button weirdness, but I don't think that's the problem as both methods of search execution are causing the same problem.
The search results are displayed in a view that is secured (It requires a username to be present - this is stored in a hidden field on the page). There are two search boxes on the site - one on the home page and one on the search results page itself (it shows a default set when you load it first time - which it does load first time fine). Both search boxes are exhibiting the same behaviour.
My site is set up in such a way that when Backbone pulls back a model, if it gets a 401 back from the API then it will send you back to the login page, so I can only think it's happening here.
Here's my view code...
function (SiansPlan, ErrorManager, RecipeSearchResult, Header, Components, TemplateSource) {
var recipeSearchView = SiansPlan.SecureView.extend({
name: 'Recipe Search',
sectionName: 'Recipes',
queryText: '',
template: Handlebars.compile(TemplateSource),
headerView: new Header({ text: 'Recipes', swatch: 'e' }),
searchBoxRegion: undefined,
$searchWrapper: undefined,
$queryHeaderMobile: undefined,
$queryHeaderDesktop: undefined,
$searchButton: undefined,
$searchInput: undefined,
$recipeSearch : undefined,
events: {
'click .link-container': 'showRecipe',
'click #searchWrapper': 'showSearch',
'click #searchButton': 'showOrPerformSearch',
'keydown #searchButton': 'performSearchOnEnter',
'keydown #recipeSearch': 'performSearchOnEnter'
},
initialize: function (options) {
this.options = options || {};
SiansPlan.SecureView.prototype.initialize.call(this, options);
this.queryText = Object.exists(this.options.query) ? this.options.query : '';
},
bindData: function () {
this.$el.html(this.template({ results: this.collection.toJSON() }));
},
render: function () {
var that = this;
if (this.isSecured()) {
this.trigger('rendering');
var params = {
success: function () {
that.bindData();
that.trigger('rendered');
},
error: function (model, xhr) {
if (Object.exists(xhr) && xhr.status == 401) {
that.applyTimedOutSecureLoginPrompt();
} else {
that.$el.html('Unable to fetch search results');
ErrorManager.handleXhr('Search failed', xhr);
}
that.trigger('rendered');
}
};
if (!Object.exists(this.collection)) {
this.collection = new RecipeSearchResult.Collection({ username: SiansPlanApp.session.username(), query: this.queryText });
}
this.collection.fetch(params);
} else {
this.applySecureLoginPrompt();
}
return this;
},
postRender: function () {
var that = this;
var queryHeader = "All recipes";
if (Object.hasValue(this.queryText)) {
queryHeader = this.collection.length + " results for '" + this.queryText + "'";
}
this.$searchWrapper = $('#searchWrapper');
this.$queryHeaderMobile = $('#queryHeaderMobile');
this.$queryHeaderDesktop = $('#queryHeaderDesktop');
this.$searchButton = $('#searchWrapper');
this.$searchInput = $('#searchInput');
this.$recipeSearch = $('#recipeSearch');
this.$queryHeaderMobile.html(queryHeader);
this.$queryHeaderDesktop.html(queryHeader);
this.$recipeSearch.val(this.queryText);
SiansPlanApp.session.waitForLoad(30, function () {
that.searchBoxRegion = new SiansPlan.Region({ el: '.recipe-search-box-container' });
that.searchBoxRegion.renderView(new Components.RecipeSearchBox({ username: SiansPlanApp.session.username(), query: that.queryText, title: 'Search' }));
});
},
performSearchOnEnter: function (e) {
if (e.keyCode == 13) {
this.showOrPerformSearch(e);
}
},
showOrPerformSearch: function (e) {
if (!this.$searchInput.is(':visible')) {
this.showSearch(e);
} else {
e.preventDefault();
var url = '/recipes/search/' + this.$recipeSearch.val();
window.SiansPlanApp.router.navigate(url, true);
}
return false;
},
showRecipe: function (e) {
e.preventDefault();
var url = $(e.target).find('a').first().attr('href');
window.SiansPlanApp.router.navigate(url, true);
},
showSearch: function (e) {
e.preventDefault();
if (!this.$searchInput.is(':visible')) {
this.$queryHeaderMobile.hide();
this.$searchInput.show();
this.$recipeSearch.focus();
this.$recipeSearch.select();
}
return false;
}
});
return recipeSearchView;
});
UPDATES
I've set up some alerts as follows in the script to see what's going on and I've discovered the following...
render: function () {
var that = this;
if (this.isSecured()) {
this.trigger('rendering');
var params = {
success: function () {
alert('Bind has succeeded!');
that.bindData();
that.trigger('rendered');
},
error: function (model, xhr) {
alert('Bind has failed!');
if (Object.exists(xhr) && xhr.status == 401) {
that.applyTimedOutSecureLoginPrompt();
} else {
that.$el.html('Unable to fetch search results');
ErrorManager.handleXhr('Search failed', xhr);
}
that.trigger('rendered');
alert(xhr.status + ' ' + xhr.responseText);
}
};
if (!Object.exists(this.collection)) {
alert('Binding new collection: ' + SiansPlanApp.session.username() + ' - ' + this.queryText);
this.collection = new RecipeSearchResult.Collection({ username: SiansPlanApp.session.username(), query: this.queryText });
}
alert('About to fetch using ' + this.collection.url());
this.collection.fetch(params);
} else {
alert('I dont appear to be secured??');
this.applySecureLoginPrompt();
}
return this;
},
When I first load the page (to show all the results) it loads fine and 'Bind Succeeded!' appears. The API call made is /api/recipes/search/{username}/
When I submit search criteria it fails ('Bind failed!') with the API call of /api/recipes/search/{username}/{query} and returns a 401.
This has me even more befuddled than before as this now looks like an API issue, but other devices are working fine and if I submit the same queries into Fiddler everything is, as expected, fine.
I've found the answer in the smallest place...
The issue was that the search criteria had an upper case letter. So, for example, when searching with 'Fish', The API generated a 301 which redirected to /api/recipes/search/{username}/fish. iOS didn't like that and reported it as a 401 (Which truly sucks!)
Related
could someone help me with one problem? I want to add a process bar when you waiting for a response from the server (Django 3.x).
Step to reproduce:
On the page 'A' we have the form.
Enter data to form.
Submit POST request by clicking to button on the page 'A'.
Waiting for getting the result on the page 'A'.
Get the result on the page 'A'.
So, I want to add process bar after 4th and before 5th points on the page 'A'. When you will get the result on the page 'A' it should disappear.
Python 3.7
Django 3.x
You can use nprogress, it's a library used for progress bars. Use this inside the interceptor where you can config it for displaying only when request is in progress until finished.
There are lots of ways to do this. I think using jquery would be easier. Basically you just need to prevent submitting the page and do an Ajax request to server. something like
<script type='text/javascript'>
$(document).ready(function () {
$("form").submit(function (e) {
// prevent page loading
e.preventDefault(e);
$('#loadinAnimation').show();
// preapre formdata
$.ajax({
type: "yourRequestType",
url: "yourUrlEndpoint",
data: formdata,
success: function (data) {
$('#loadinAnimation').hide();
// do rest of the work with data
}
});
});
});
</script>
and show appropriate loading animation in your html part
<div id='loadinAnimation' style='display:none'>
<div>loading gif</div>
</div>
You can also do it using UiKit Library in Javascript on your Django Template Page.
Below code is when a file is Uploaded
In your template file (template.html)
<body>
..
<form>
<progress id="js-progressbar" class="uk-progress" value="0" max="100" hidden></progress>
...
<div class="uk-alert-danger uk-margin-top uk-hidden" id="upload_error" uk-alert></div>
...
</form>
</head>
<script type="text/javascript">
$(document).ready(function(){
var bar = document.getElementById('js-progressbar');
UIkit.upload('.js-upload-list', {
url: '',
name : "customer-docs",
params :{
"csrfmiddlewaretoken":"{{csrf_token}}"
},
method : "POST",
concurrent:1,
allow:'*.(csv|xlsx)',
beforeSend: function (environment) {
console.log('beforeSend', arguments);
// The environment object can still be modified here.
// var {data, method, headers, xhr, responseType} = environment;
},
beforeAll: function (args,files) {
console.log('beforeAll', arguments);
},
load: function () {
console.log('load', arguments);
},
error: function (files) {
console.log("---------------")
},
complete: function () {
console.log('complete', arguments);
},
loadStart: function (e) {
console.log('loadStart', arguments);
bar.removeAttribute('hidden');
bar.max = e.total;
bar.value = e.loaded;
},
progress: function (e) {
console.log('progress', arguments);
bar.max = e.total;
bar.value = e.loaded;
},
loadEnd: function (e) {
console.log('loadEnd', arguments);
bar.max = e.total;
bar.value = e.loaded;
},
completeAll: function (data) {
console.log('completeAll', arguments);
console.log('completeAll', data);
let redirect_loc = ""
setTimeout(function () {
bar.setAttribute('hidden', 'hidden');
}, 1000);
// This is the response from your POST method of views.py
data.responseText = JSON.parse(data.responseText)
if(data.responseText.status == 201){
// swal is another library to show sweet alert pop ups
swal({
icon: data.responseText.status_icon,
closeOnClickOutside: true,
text: data.responseText.message,
buttons: {
Done: true
},
}).then((value) => {
switch (value) {
case "Done":
window.location.href = ""
break;
}
});
}
else if(data.responseText.status == 500){
swal({
icon: data.responseText.status_icon,
closeOnClickOutside: true,
text: data.responseText.message,
buttons: {
Ok: true
},
}).then((value) => {
switch (value) {
case "Ok":
window.location.href = ""
break;
}
});
}
}
});
// This block of code is to restrict user to upload only specific FILE formats (below example is for CSV & XLSX files)
(function() {
var _old_alert = window.alert;
window.alert = function(e) {
console.log(e)
if(e.includes("csv|xlsx") || e.includes("Invalid file type")) {
$("#upload_error").html("Invalid file format. Valid formats are CSV, XLSX").removeClass('uk-hidden')
}else if(e.includes("Internal Server Error")) {
$("#upload_error").html("Internal Server Error Kindly upload Documents again").removeClass('uk-hidden')
}
else {
_old_alert.apply(window,arguments);
$("#upload_error").addClass('uk-hidden').html("")
}
};
})();
});
</script>
On your views.py you can do your computation and once done, you can return a response like below
resp_json = {
"status" : 201,
"status_icon" : "success",
"url" : "/",
"message": message
}
return HttpResponse(json.dumps(resp_json))
For more info on SWAL (Sweet Alerts), visit https://sweetalert.js.org/guides/
I tried to include a button (created from user event) on Sales order. Upon clicking it, Invoice will be generated. As soon as the button is hit, ther comes an error and invoice doesnt get generated. Can anyone help me with this?
//Client script
function pageInit() {
}
function csForButton(ctx) {
var rec = curr.get();
var customer = rec.getValue({ "fieldId": "customer" });
log.error({
title: 'customer',
details: customer
});
var scriptURL = url.resolveScript({
"scriptId": "customscript_button_task_sl",
"deploymentId": "customdeploy_button_task_sl"
});
console.log('scriptURL', scriptURL);
window.onbeforeunload = null;
window.open(scriptURL + '&id=' + rec.id);
}
return {
pageInit: pageInit,
csForButton: csForButton
};
//User Event Script
function beforeLoad(ctx) {
//if (context.type == context.UserEventType.VIEW) {
addbutton(ctx);
// }
}
function addbutton(ctx) {
try {
var rec = ctx.newRecord;//record object, now we can get its properties through it(e.g. fields)
var statusOfTrans = rec.getValue({ fieldId: 'status' });
log.error('statusOfTrans', statusOfTrans);
ctx.form.clientScriptFileId = "16474"
if (ctx.type == "view" && statusOfTrans == 'Pending Fulfillment') {
ctx.form.addButton({
id: 'custpage_make_invoice',
label: 'create invoice!',
functionName: 'csForButton'
});
}
}
catch (error) {
log.error('addbutton', error);
}
}
return {
beforeLoad: beforeLoad,
}
//Suitelet Script
function onRequest(ctx) {
try {
var req = ctx.request;
var res = ctx.response;
if (req.method == 'GET') {
var objRecord = rec.transform({
fromType: rec.Type.SALES_ORDER,
fromId: req.parameters.id,
toType: rec.Type.INVOICE,
isDynamic: true,
});
objRecord.save({
ignoreMandatoryFields: true
});
res.write({output: 'Invoice created'});
}
} catch (error) {
log.error('onRequest', error);
}
}
return {
'onRequest': onRequest
}
error (in suitelet):
{"type":"error.SuiteScriptError","name":"USER_ERROR","message":"You must enter at least one line item for this transaction.","stack":["anonymous(N/serverRecordService)","onRequest(/SuiteScripts/button SL.js:39)"],"cause":{"type":"internal error","code":"USER_ERROR","details":"You must enter at least one line item for this transaction.","userEvent":null,"stackTrace":["anonymous(N/serverRecordService)","onRequest(/SuiteScripts/button SL.js:39)"],"notifyOff":false},"id":"","notifyOff":false,"userFacing":false}
As Error says to include atleast 1 line but i wanted it to be generated automatically. I am new in suitescript and taking all the help from the documentation. Can anyone jst guide me what is wrong i m doing?
Thank u
You need the SO status to be Pending Billing. If the status of the SO is Pending Fulfillment, then no line items are ready to be invoiced.
I am currently trying to get a kendo ui autocomplete to show results even if a user enters first and second name. At the moment i currently have my code working if the autocomplete contains first or second name. I would like the autocomplete to still show results for e.g if 'Peter S' was entered it would still show a results for 'Peter Smith' and 'Peter Samsung'. If someone could have a look at my code and maybe point me in the correct direction that would be great. I have spent alot of time on this and i think i might be taking the wrong approach.
Code below:
$("#FeeEarnerEmailSend").kendoAutoComplete({
dataSource: new kendo.data.DataSource({
serverFiltering: true,
transport: {
read: "/" + prefix + "/api/Session/GetEmployees",
parameterMap: function () {
return { id: $("#FeeEarnerEmailSend").data("kendoAutoComplete").value() };
}
}
}),
dataTextField: 'FullName',
filter: "contains",
//placeholder: "Search...",
minLength: 3,
suggest: true,
select: function (e) {
var employeeAutoComplete = e.sender;
// this var is used in the Search button click event
selectedEmployeeDataItem = employeeAutoComplete.dataItem(e.item.index());
},
change: function () {
if ($.trim($("#FeeEarnerEmailSend").val()) == "") {
selectedEmployeeDataItem = null;
}
},
dataBound: function (e) {
selectedEmployeeDataItem = e.sender.dataItem(0);
}
});
This is my Csharp code, i think the issue may lie here within my linq which needs edited to achieve this?
[HttpGet]
[Route("api/Session/GetEmployees")]
public IHttpActionResult GetEmployees(string id)
{
logger.Trace("Get Employees - {0} - based on prefix", id);
try
{
DirectoryContext context = new DirectoryContext(new Uri(ConfigurationManager.AppSettings["DirectoryServiceUrl"]));
var result = from q in context.Employees
where q.Surname.Contains(id) || q.KnownAs.Contains(id)
orderby q.Surname, q.KnownAs
select new
{
NetworkId = q.NetworkID,
FullName = String.Format("{0} {1}", q.Surname.ToUpper(), q.KnownAs),
EmailAddress = q.EmailAddress
};
logger.Info("Returning Employees - {0}", id);
return Ok(result.ToList());
}
catch (Exception ex)
{
logger.Error(ex.Message, ex);
return InternalServerError(ex);
}
}
I want my collection to fail if the server/json return a specific STATUS (e.g. no results).
The problem: The default error-handler is not called (cause the collection successfully fetches the json. So my idea is use the parse function to look for an error-code in the json.
But how to I trigger the error-method and notify my view (and stop to collection trying to create models)
/*global define*/
define([
'underscore',
'backbone',
'models/mymodel'
], function (_, Backbone, MyModel) {
'use strict';
var SomeCollection = Backbone.Collection.extend({
model: MyModel,
value: null,
url: function() {
return url: "data/list.json";
},
initialize: function(models, options) {
this.zipcode = options.zipcode;
},
parse: function(response, xhr) {
if(response.status == "OK") {
console.info("Status: "+response.status);
return response.results;
} else {
console.warn("Status: "+response.status+" – Message: "+response.message);
this.trigger('fail') // does not work
return response;
}
}
});
return SomeCollection;
});
I have a post on my blog about this kind of things, unfortunately it's in portuguese, but maybe google translate helps you.
http://www.rcarvalhojs.com/dicas/de/backbone/2014/06/24/gerenciando-o-estado-da-aplicacao.html
I like to handle this, in this way
GetSomething:->
#result = #fetch(
success:()=>
#trigger "succesthing1" if #result .status is 204
#trigger "successThing2" if #result .status is 200
error:()=>
#trigger "errorThing" if #result .status is 401
)
Now i can listen for these trigger inside the view and take the correct action for a specific the result from server
There are currently I subscribe for of the Backbone sync, by sending events according to the promise that the request returned, see example below:
(function(Backbone) {
var methods, _sync;
_sync = Backbone.sync;
methods = {
beforeSend: function() {
return this.trigger("sync:start", this);
},
error: function() {
return this.trigger("sync:error", this);
},
complete: function() {
return this.trigger("sync:stop", this);
}
};
Backbone.sync = function(method, entity, options) {
var sync;
if (options == null) {
options = {};
}
_.defaults(options, {
beforeSend: _.bind(methods.beforeSend, entity),
error: _.bind(methods.error, entity)
complete: _.bind(methods.complete, entity)
});
sync = _sync(method, entity, options);
if (!entity._fetch && method === "read") {
return entity._fetch = sync;
}
};
})(Backbone);
Hope this helps.
I'm not sure how to express this in code, as I can't seem to locate the problem, but my issue is that Backbone.history seems to be recording two items when a user clicks on a list item in my app.
This is not consistent.
My app has a 4 item navigation at the bottom that links to 4 main sections (the first one being home - routed to '/'). If I load up the app, go to one of the other navigation pages, then click the 'Home' button again and then click one of the navigation options I get a list of items to choose from. If I then choose one two entries are added - Firstly, for some reason, a reference to the home route with /# at the end and then the route for the item I clicked.
The end result is that 'back' then inexplicably takes me to the home page.
If it helps, my router looks like this...
var siansplanRouter = Backbone.Router.extend({
initialize: function () {
var that = this;
this.routesHit = 0;
//keep count of number of routes handled by your application
Backbone.history.on('route', function() { that.routesHit++; }, this);
window.SiansPlanApp.render();
window.SiansPlanApp.router = this;
},
routes: {
'': 'showHome',
'home': 'showHome',
'hub': 'showHome',
'samples': 'showJqmSamples',
'mealplanner': 'showCurrentMealPlanner',
'mealplanner/:planId': 'showMealPlanner',
'recipes': 'showRecipeSearch',
'recipes/:recipeId': 'showRecipe',
'settings': 'showSettings',
'versioninfo': 'showVersionInfo',
'*other': 'showHome'
},
routesHit: 0,
back: function() {
if(this.routesHit > 1) {
window.history.back();
} else {
//otherwise go to the home page. Use replaceState if available so
//the navigation doesn't create an extra history entry
this.navigate('/', { trigger: true, replace: true });
}
},
showHome: function () {
SiansPlanApp.renderHome();
},
showJqmSamples: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Hub.Samples());
},
showMealPlanner: function (planId) {
SiansPlanApp.renderView(new SiansPlanApp.views.Planner.MealPlanner({ id: planId }));
},
showCurrentMealPlanner: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Planner.MealPlanner({ current: true }));
},
showRecipeSearch: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.Recipes.Search());
},
showRecipe: function (recipeId) {
SiansPlanApp.renderView(new SiansPlanApp.views.Recipes.Recipe({ id: recipeId }));
},
showSettings: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.System.Settings());
},
showVersionInfo: function () {
SiansPlanApp.renderView(new SiansPlanApp.views.About.VersionInfo.ListView());
}
});
I've got some basic elements in a kick off file too here...
define(['router', 'regions/r-app', 'jquery', 'domReady'],
function (SiansPlanRouter, AppRegion) {
var run = function () {
// Global click event handler to pass through links to navigate
$(document).on("click", "a:not([data-bypass])", function (e) {
var href = { prop: $(this).prop("href"), attr: $(this).attr("href") };
var root = location.protocol + "//" + location.host + SiansPlanApp.root;
if (href.prop && href.prop.slice(0, root.length) === root) {
e.preventDefault();
Backbone.history.navigate(href.attr, true);
}
});
$.ajaxPrefilter(function (options, originalOptions, jqXhr) {
//options.url = '/api' + options.url;
});
// Create the global namespace region object.
window.SiansPlanApp = new AppRegion();
// Adds the authorization header to all of the API requests.
$(document).ajaxSend(function (e, xhr, options) {
xhr.setRequestHeader("Authorization", 'SiansPlan ' + SiansPlanApp.cookies.getSessionData());
});
// Load up session data if any is present yet - this can't happen until the XHR headers are set up.
SiansPlanApp.session.loadSession();
// Instantiate the router.
window.SiansPlanApp.router = new SiansPlanRouter();
// Boot up the app:
Backbone.history.start();
};
return {
run: run
};
});