From 585c7339de9010ed4cb242490bd2f19edd6967ac Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Sat, 29 Apr 2023 17:16:14 +0100 Subject: [PATCH 1/6] Initial Commit --- core/modules/startup/rootwidget.js | 5 + core/modules/utils/dom/http.js | 144 +++++++++++++++++- core/modules/wiki.js | 8 + ...essage_ tm-http-request Example Zotero.tid | 112 ++++++++++++++ .../WidgetMessage_ tm-http-request.tid | 49 ++++++ .../tiddlers/messages/config-zotero-group.tid | 2 + 6 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid create mode 100644 editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid create mode 100644 editions/tw5.com/tiddlers/messages/config-zotero-group.tid diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js index a8ad5f8c6..4761fef85 100644 --- a/core/modules/startup/rootwidget.js +++ b/core/modules/startup/rootwidget.js @@ -20,6 +20,11 @@ exports.before = ["story"]; exports.synchronous = true; exports.startup = function() { + // Install the HTTP client event handler + $tw.httpClient = new $tw.utils.HttpClient(); + $tw.rootWidget.addEventListener("tm-http-request",function(event) { + $tw.httpClient.handleHttpRequest(event); + }); // Install the modal message mechanism $tw.modal = new $tw.utils.Modal($tw.wiki); $tw.rootWidget.addEventListener("tm-modal",function(event) { diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js index 6e07b1040..797419b81 100644 --- a/core/modules/utils/dom/http.js +++ b/core/modules/utils/dom/http.js @@ -3,7 +3,7 @@ title: $:/core/modules/utils/dom/http.js type: application/javascript module-type: utils -Browser HTTP support +HTTP support \*/ (function(){ @@ -13,11 +13,127 @@ Browser HTTP support "use strict"; /* -A quick and dirty HTTP function; to be refactored later. Options are: +Manage tm-http-request events. Options are: +wiki - the wiki object to use +*/ +function HttpClient(options) { + options = options || {}; +} + +HttpClient.prototype.handleHttpRequest = function(event) { + console.log("Initiating an HTTP request",event) + var self = this, + wiki = event.widget.wiki, + paramObject = event.paramObject || {}, + url = paramObject.url, + completionActions = paramObject.oncompletion || "", + progressActions = paramObject.onprogress || "", + bindStatus = paramObject["bind-status"], + bindProgress = paramObject["bind-progress"], + method = paramObject.method || "GET", + HEADER_PARAMETER_PREFIX = "header-", + QUERY_PARAMETER_PREFIX = "query-", + PASSWORD_HEADER_PARAMETER_PREFIX = "password-header-", + PASSWORD_QUERY_PARAMETER_PREFIX = "password-query-", + CONTEXT_VARIABLE_PARAMETER_PREFIX = "var-", + requestHeaders = {}, + contextVariables = {}, + setBinding = function(title,text) { + if(title) { + wiki.addTiddler(new $tw.Tiddler({title: title, text: text})); + } + }; + if(url) { + setBinding(bindStatus,"pending"); + setBinding(bindProgress,"0"); + $tw.utils.each(paramObject,function(value,name) { + // Look for query- parameters + if(name.substr(0,QUERY_PARAMETER_PREFIX.length) === QUERY_PARAMETER_PREFIX) { + url = $tw.utils.setQueryStringParameter(url,name.substr(QUERY_PARAMETER_PREFIX.length),value); + } + // Look for header- parameters + if(name.substr(0,HEADER_PARAMETER_PREFIX.length) === HEADER_PARAMETER_PREFIX) { + requestHeaders[name.substr(HEADER_PARAMETER_PREFIX.length)] = value; + } + // Look for password-header- parameters + if(name.substr(0,PASSWORD_QUERY_PARAMETER_PREFIX.length) === PASSWORD_QUERY_PARAMETER_PREFIX) { + url = $tw.utils.setQueryStringParameter(url,name.substr(PASSWORD_QUERY_PARAMETER_PREFIX.length),$tw.utils.getPassword(value) || ""); + } + // Look for password-query- parameters + if(name.substr(0,PASSWORD_HEADER_PARAMETER_PREFIX.length) === PASSWORD_HEADER_PARAMETER_PREFIX) { + requestHeaders[name.substr(PASSWORD_HEADER_PARAMETER_PREFIX.length)] = $tw.utils.getPassword(value) || ""; + } + // Look for var- parameters + if(name.substr(0,CONTEXT_VARIABLE_PARAMETER_PREFIX.length) === CONTEXT_VARIABLE_PARAMETER_PREFIX) { + contextVariables[name.substr(CONTEXT_VARIABLE_PARAMETER_PREFIX.length)] = value; + } + }); + // Set the request tracker tiddler + var requestTrackerTitle = wiki.generateNewTitle("$:/temp/HttpRequest"); + wiki.addTiddler({ + title: requestTrackerTitle, + tags: "$:/tags/HttpRequest", + text: JSON.stringify({ + url: url, + type: method, + status: "inprogress", + headers: requestHeaders, + data: paramObject.body + }) + }); + $tw.utils.httpRequest({ + url: url, + type: method, + headers: requestHeaders, + data: paramObject.body, + callback: function(err,data,xhr) { + var success = (xhr.status >= 200 && xhr.status < 300) ? "complete" : "error", + headers = {}; + $tw.utils.each(xhr.getAllResponseHeaders().split("\r\n"),function(line) { + var pos = line.indexOf(":"); + if(pos !== -1) { + headers[line.substr(0,pos)] = line.substr(pos + 1).trim(); + } + }); + setBinding(bindStatus,success); + setBinding(bindProgress,"100"); + var results = { + status: xhr.status.toString(), + statusText: xhr.statusText, + error: (err || "").toString(), + data: (data || "").toString(), + headers: JSON.stringify(headers) + }; + // Update the request tracker tiddler + wiki.addTiddler(new $tw.Tiddler(wiki.getTiddler(requestTrackerTitle),{ + status: success, + })); + wiki.invokeActionString(completionActions,undefined,$tw.utils.extend({},contextVariables,results),{parentWidget: $tw.rootWidget}); + // console.log("Back!",err,data,xhr); + }, + progress: function(lengthComputable,loaded,total) { + if(lengthComputable) { + setBinding(bindProgress,"" + Math.floor((loaded/total) * 100)) + } + wiki.invokeActionString(progressActions,undefined,{ + lengthComputable: lengthComputable ? "yes" : "no", + loaded: loaded, + total: total + },{parentWidget: $tw.rootWidget}); + } + }); + } +}; + +exports.HttpClient = HttpClient; + +/* +Make an HTTP request. Options are: url: URL to retrieve headers: hashmap of headers to send type: GET, PUT, POST etc callback: function invoked with (err,data,xhr) + progress: optional function invoked with (lengthComputable,loaded,total) returnProp: string name of the property to return as first argument of callback */ exports.httpRequest = function(options) { @@ -83,8 +199,16 @@ exports.httpRequest = function(options) { options.callback($tw.language.getString("Error/XMLHttpRequest") + ": " + this.status,null,this); } }; + // Handle progress + if(options.progress) { + request.onprogress = function(event) { + console.log("Progress event",event) + options.progress(event.lengthComputable,event.loaded,event.total); + }; + } // Make the request request.open(type,url,true); + // Headers if(headers) { $tw.utils.each(headers,function(header,headerTitle,object) { request.setRequestHeader(headerTitle,header); @@ -96,6 +220,7 @@ exports.httpRequest = function(options) { if(!hasHeader("X-Requested-With") && !isSimpleRequest(type,headers)) { request.setRequestHeader("X-Requested-With","TiddlyWiki"); } + // Send data try { request.send(data); } catch(e) { @@ -104,4 +229,19 @@ exports.httpRequest = function(options) { return request; }; +exports.setQueryStringParameter = function(url,paramName,paramValue) { + var URL = $tw.browser ? window.URL : require("url").URL, + newUrl; + try { + newUrl = new URL(url); + } catch(e) { + } + if(newUrl && paramName) { + newUrl.searchParams.set(paramName,paramValue || ""); + return newUrl.toString(); + } else { + return url; + } +}; + })(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 6ae16a2b4..85cacaa32 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -1415,6 +1415,14 @@ exports.checkTiddlerText = function(title,targetText,options) { return text === targetText; } +/* +Execute an action string without an associated context widget +*/ +exports.invokeActionString = function(actions,event,variables,options) { + var widget = this.makeWidget(null,{parentWidget: options.parentWidget}); + widget.invokeActionString(actions,null,event,variables); +}; + /* Read an array of browser File objects, invoking callback(tiddlerFieldsArray) once they're all read */ diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid new file mode 100644 index 000000000..9419f526d --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid @@ -0,0 +1,112 @@ +title: WidgetMessage: tm-http-request Example - Zotero +tags: $:/tags/Macro + +\procedure select-zotero-group() +Specify the Zotero group ID to import +<$edit-text tiddler="$:/config/zotero-group" tag="input"/> or +<$select tiddler="$:/config/zotero-group"> + + + + +\end + +\procedure zotero-save-item(item) +<$action-createtiddler + $basetitle={{{ =[[_zotero_import ]] =[jsonget[key]] =[[ ]] =[jsonget[title]] +[join[]] }}} + text={{{ [jsonget[title]] }}} + tags="$:/tags/ZoteroImport" +> + <$action-setmultiplefields $tiddler=<> $fields="[jsonindexes[]addprefix[zotero-]]" $values="[jsonindexes[]] :map[jsongetelse[.XXXXX.]]"/> + <$list filter="[jsonindexes[creators]]" variable="creatorIndex"> + <$action-setmultiplefields $tiddler=<> $fields="[jsonget[creators],,[creatorType]addprefix[zotero-]]" $values="[jsonget[creators],,[lastName]] [jsonget[creators],,[firstName]] +[join[, ]] :else[jsonget[creators],,[name]] "/> + + +\end zotero-save-item + +\procedure zotero-save-items(data) +<$list filter="[jsonindexes[]] :map[jsonextract,[data]]" variable="item"> + <$macrocall $name="zotero-save-item" item=<>/> + +\end zotero-save-items + +\procedure zotero-get-items(start:"0",limit:"25") + +\procedure completion() +\import [[$:/core/ui/PageMacros]] [all[shadows+tiddlers]tag[$:/tags/Macro]!has[draft.of]] + <$action-log msg="In completion"/> + <$action-log/> + + <$list filter="[compare:number:gteq[200]compare:number:lteq[299]]" variable="ignore"> + + <$macrocall $name="zotero-save-items" data=<>/> + + <$list filter="[jsonget[total-results]subtractsubtractcompare:number:gt[0]]" variable="ignore"> + <$macrocall $name="zotero-get-items" start={{{ [add] }}} limit=<>/> + + +\end completion + +\procedure progress() +\import [[$:/core/ui/PageMacros]] [all[shadows+tiddlers]tag[$:/tags/Macro]!has[draft.of]] + <$action-log message="In progress-actions"/> +\end progress + +\procedure request-url() +\rules only transcludeinline transcludeblock filteredtranscludeinline filteredtranscludeblock +https://api.zotero.org/groups/{{$:/config/zotero-group}}/items/ +\end request-url + +<$wikify name="url" text=<>> + <$action-sendmessage + $message="tm-http-request" + url=<> + method="GET" + query-format="json" + query-sort="title" + query-start=<> + query-limit=<> + header-accept="application/json" + bind-status="$:/temp/zotero/status" + bind-progress="$:/temp/zotero/progress" + oncompletion=<> + onprogress=<> + var-start=<> + var-limit=<> + /> + +\end + +\procedure zotero-actions() +<$macrocall $name="zotero-get-items" start="0" limit="50"/> +\end + + +<> + +<$button actions=<>> +Start import from Zotero group + + +<$list filter="[tag[$:/tags/ZoteroImport]limit[1]]" variable="ignore"> + +!! Imported Tiddlers + +<$button> +<$action-deletetiddler $filter="[tag[$:/tags/ZoteroImport]]"/> +Delete these tiddlers + + +Export: <$macrocall $name="exportButton" exportFilter="[tag[$:/tags/ZoteroImport]]" lingoBase="$:/language/Buttons/ExportTiddlers/"/> + + + +
    +<$list filter="[tag[$:/tags/ZoteroImport]]"> +
  1. +<$link> +<$view field="title"/> + +
  2. + +
diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid new file mode 100644 index 000000000..f74fdfb55 --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid @@ -0,0 +1,49 @@ +caption: tm-http-request +created: 20230429161453032 +modified: 20230429161453032 +tags: Messages +title: WidgetMessage: tm-http-request +type: text/vnd.tiddlywiki + +The ''tm-http-request'' message is used to make an HTTP request to a server. + +It uses the following properties on the `event` object: + +|!Name |!Description | +|param |Not used | +|paramObject |Hashmap of parameters (see below) | + +The following parameters are used: + +|!Name |!Description | +|method |HTTP method (eg "GET", "POST") | +|body |String data to be sent with the request | +|query-* |Query string parameters with string values | +|header-* |Headers with string values | +|password-header-* |Headers with values taken from the password store | +|password-query-* |Query string parameters with values taken from the password store | +|var-* |Variables to be passed to the completion and progress handlers (without the "var-" prefix) | +|bind-status |Title of tiddler to which the status of the request ("pending", "complete", "error") should be bound | +|bind-progress |Title of tiddler to which the progress of the request (0 to 100) should be bound | +|oncompletion |Action strings to be executed when the request completes | +|onprogress |Action strings to be executed when progress is reported | + +The following variables are passed to the completion handler: + +|!Name |!Description | +|status |HTTP result status code (see [[MDN|https://developer.mozilla.org/en-US/docs/Web/HTTP/Status]]) | +|statusText |HTTP result status text | +|error |Error string | +|data |Returned data | +|headers |Response headers as a JSON object | + +The following variables are passed to the progress handler: + +|!Name |!Description | +|lengthComputable |Whether the progress loaded and total figures are valid - "yes" or "no" | +|loaded |Number of bytes loaded so far | +|total |Total number bytes to be loaded | + +!! Examples + +* [[Zotero's|https://www.zotero.org/]] API for retrieving reference items: [[WidgetMessage: tm-http-request Example - Zotero]] diff --git a/editions/tw5.com/tiddlers/messages/config-zotero-group.tid b/editions/tw5.com/tiddlers/messages/config-zotero-group.tid new file mode 100644 index 000000000..2215c496a --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/config-zotero-group.tid @@ -0,0 +1,2 @@ +title: $:/config/zotero-group +text: 4813312 \ No newline at end of file From 0adc0518a6741c7b4ef34cb55af9418abd98cf9c Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Mon, 1 May 2023 17:04:35 +0100 Subject: [PATCH 2/6] HttpClient object shouldn't need to know about events --- core/modules/startup/rootwidget.js | 25 ++++++++- core/modules/utils/dom/http.js | 84 +++++++++++++++--------------- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js index 4761fef85..1f696ae34 100644 --- a/core/modules/startup/rootwidget.js +++ b/core/modules/startup/rootwidget.js @@ -22,8 +22,31 @@ exports.synchronous = true; exports.startup = function() { // Install the HTTP client event handler $tw.httpClient = new $tw.utils.HttpClient(); + var getPropertiesWithPrefix = function(properties,prefix) { + var result = Object.create(null); + $tw.utils.each(properties,function(value,name) { + if(name.indexOf(prefix) === 0) { + result[name.substring(prefix.length)] = properties[name]; + } + }); + return result; + }; $tw.rootWidget.addEventListener("tm-http-request",function(event) { - $tw.httpClient.handleHttpRequest(event); + var params = event.paramObject || {}; + $tw.httpClient.initiateHttpRequest({ + wiki: event.widget.wiki, + url: params.url, + method: params.method, + oncompletion: params.oncompletion, + onprogress: params.onprogress, + bindStatus: params["bind-status"], + bindProgress: params["bind-progress"], + variables: getPropertiesWithPrefix(params,"var-"), + headers: getPropertiesWithPrefix(params,"header-"), + passwordHeaders: getPropertiesWithPrefix(params,"password-header-"), + queryStrings: getPropertiesWithPrefix(params,"query-"), + passwordQueryStrings: getPropertiesWithPrefix(params,"password-query-") + }); }); // Install the modal message mechanism $tw.modal = new $tw.utils.Modal($tw.wiki); diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js index 797419b81..07bbe86c1 100644 --- a/core/modules/utils/dom/http.js +++ b/core/modules/utils/dom/http.js @@ -13,31 +13,39 @@ HTTP support "use strict"; /* -Manage tm-http-request events. Options are: -wiki - the wiki object to use +Manage tm-http-request events */ function HttpClient(options) { options = options || {}; } -HttpClient.prototype.handleHttpRequest = function(event) { - console.log("Initiating an HTTP request",event) +/* +Initiate an HTTP request. Options: +wiki: wiki to be used for executing action strings +url: URL for request +method: method eg GET, POST +body: text of request body +oncompletion: action string to be invoked on completion +onprogress: action string to be invoked on progress updates +bindStatus: optional title of tiddler to which status ("pending", "complete", "error") should be written +bindProgress: optional title of tiddler to which the progress of the request (0 to 100) should be bound +variables: hashmap of variable name to string value passed to action strings +headers: hashmap of header name to header value to be sent with the request +passwordHeaders: hashmap of header name to password store name to be sent with the request +queryStrings: hashmap of query string parameter name to parameter value to be sent with the request +passwordQueryStrings: hashmap of query string parameter name to password store name to be sent with the request +*/ +HttpClient.prototype.initiateHttpRequest = function(options) { + console.log("Initiating an HTTP request",options) var self = this, - wiki = event.widget.wiki, - paramObject = event.paramObject || {}, - url = paramObject.url, - completionActions = paramObject.oncompletion || "", - progressActions = paramObject.onprogress || "", - bindStatus = paramObject["bind-status"], - bindProgress = paramObject["bind-progress"], - method = paramObject.method || "GET", - HEADER_PARAMETER_PREFIX = "header-", - QUERY_PARAMETER_PREFIX = "query-", - PASSWORD_HEADER_PARAMETER_PREFIX = "password-header-", - PASSWORD_QUERY_PARAMETER_PREFIX = "password-query-", - CONTEXT_VARIABLE_PARAMETER_PREFIX = "var-", + wiki = options.wiki, + url = options.url, + completionActions = options.oncompletion, + progressActions = options.onprogress, + bindStatus = options["bind-status"], + bindProgress = options["bind-progress"], + method = options.method || "GET", requestHeaders = {}, - contextVariables = {}, setBinding = function(title,text) { if(title) { wiki.addTiddler(new $tw.Tiddler({title: title, text: text})); @@ -46,27 +54,17 @@ HttpClient.prototype.handleHttpRequest = function(event) { if(url) { setBinding(bindStatus,"pending"); setBinding(bindProgress,"0"); - $tw.utils.each(paramObject,function(value,name) { - // Look for query- parameters - if(name.substr(0,QUERY_PARAMETER_PREFIX.length) === QUERY_PARAMETER_PREFIX) { - url = $tw.utils.setQueryStringParameter(url,name.substr(QUERY_PARAMETER_PREFIX.length),value); - } - // Look for header- parameters - if(name.substr(0,HEADER_PARAMETER_PREFIX.length) === HEADER_PARAMETER_PREFIX) { - requestHeaders[name.substr(HEADER_PARAMETER_PREFIX.length)] = value; - } - // Look for password-header- parameters - if(name.substr(0,PASSWORD_QUERY_PARAMETER_PREFIX.length) === PASSWORD_QUERY_PARAMETER_PREFIX) { - url = $tw.utils.setQueryStringParameter(url,name.substr(PASSWORD_QUERY_PARAMETER_PREFIX.length),$tw.utils.getPassword(value) || ""); - } - // Look for password-query- parameters - if(name.substr(0,PASSWORD_HEADER_PARAMETER_PREFIX.length) === PASSWORD_HEADER_PARAMETER_PREFIX) { - requestHeaders[name.substr(PASSWORD_HEADER_PARAMETER_PREFIX.length)] = $tw.utils.getPassword(value) || ""; - } - // Look for var- parameters - if(name.substr(0,CONTEXT_VARIABLE_PARAMETER_PREFIX.length) === CONTEXT_VARIABLE_PARAMETER_PREFIX) { - contextVariables[name.substr(CONTEXT_VARIABLE_PARAMETER_PREFIX.length)] = value; - } + $tw.utils.each(options.queryStrings,function(value,name) { + url = $tw.utils.setQueryStringParameter(url,name,value); + }); + $tw.utils.each(options.passwordQueryStrings,function(value,name) { + url = $tw.utils.setQueryStringParameter(url,name,$tw.utils.getPassword(value) || ""); + }); + $tw.utils.each(options.headers,function(value,name) { + requestHeaders[name] = value; + }); + $tw.utils.each(options.passwordHeaders,function(value,name) { + requestHeaders[name] = $tw.utils.getPassword(value) || ""; }); // Set the request tracker tiddler var requestTrackerTitle = wiki.generateNewTitle("$:/temp/HttpRequest"); @@ -78,14 +76,14 @@ HttpClient.prototype.handleHttpRequest = function(event) { type: method, status: "inprogress", headers: requestHeaders, - data: paramObject.body + data: options.body }) }); $tw.utils.httpRequest({ url: url, type: method, headers: requestHeaders, - data: paramObject.body, + data: options.body, callback: function(err,data,xhr) { var success = (xhr.status >= 200 && xhr.status < 300) ? "complete" : "error", headers = {}; @@ -97,7 +95,7 @@ HttpClient.prototype.handleHttpRequest = function(event) { }); setBinding(bindStatus,success); setBinding(bindProgress,"100"); - var results = { + var resultVariables = { status: xhr.status.toString(), statusText: xhr.statusText, error: (err || "").toString(), @@ -108,7 +106,7 @@ HttpClient.prototype.handleHttpRequest = function(event) { wiki.addTiddler(new $tw.Tiddler(wiki.getTiddler(requestTrackerTitle),{ status: success, })); - wiki.invokeActionString(completionActions,undefined,$tw.utils.extend({},contextVariables,results),{parentWidget: $tw.rootWidget}); + wiki.invokeActionString(completionActions,undefined,$tw.utils.extend({},options.variables,resultVariables),{parentWidget: $tw.rootWidget}); // console.log("Back!",err,data,xhr); }, progress: function(lengthComputable,loaded,total) { From be1882dd4c2956e70693356420f99c1fc3c16984 Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Mon, 1 May 2023 17:46:04 +0100 Subject: [PATCH 3/6] Add support for cancelling HTTP requests --- core/modules/startup/rootwidget.js | 3 + core/modules/utils/dom/http.js | 127 ++++++++++++------ ...essage_ tm-http-request Example Zotero.tid | 4 + 3 files changed, 93 insertions(+), 41 deletions(-) diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js index 1f696ae34..70d262a2c 100644 --- a/core/modules/startup/rootwidget.js +++ b/core/modules/startup/rootwidget.js @@ -48,6 +48,9 @@ exports.startup = function() { passwordQueryStrings: getPropertiesWithPrefix(params,"password-query-") }); }); + $tw.rootWidget.addEventListener("tm-http-cancel-all-requests",function(event) { + $tw.httpClient.cancelAllHttpRequests(); + }); // Install the modal message mechanism $tw.modal = new $tw.utils.Modal($tw.wiki); $tw.rootWidget.addEventListener("tm-modal",function(event) { diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js index 07bbe86c1..14270eef7 100644 --- a/core/modules/utils/dom/http.js +++ b/core/modules/utils/dom/http.js @@ -17,8 +17,40 @@ Manage tm-http-request events */ function HttpClient(options) { options = options || {}; + this.nextId = 1; + this.requests = []; // Array of {id: string,request: HttpClientRequest} } +HttpClient.prototype.initiateHttpRequest = function(options) { + var id = this.nextId, + request = new HttpClientRequest(options); + this.nextId += 1; + this.requests.push({id: id, request: request}); + request.send(); + return id; +}; + +HttpClient.prototype.cancelAllHttpRequests = function() { + var self = this; + $tw.utils.each(this.requests,function(requestInfo,index) { + requestInfo.request.cancel(); + }); + this.requests = []; +}; + +HttpClient.prototype.cancelHttpRequest = function(targetId) { + var targetIndex = null; + $tw.utils.each(this.requests,function(requestInfo,index) { + if(requestInfo.id === targetId) { + targetIndex = index; + } + }); + if(targetIndex !== null) { + this.requests[targetIndex].request.cancel(); + this.requests.splice(targetIndex,1); + } +}; + /* Initiate an HTTP request. Options: wiki: wiki to be used for executing action strings @@ -35,55 +67,62 @@ passwordHeaders: hashmap of header name to password store name to be sent with t queryStrings: hashmap of query string parameter name to parameter value to be sent with the request passwordQueryStrings: hashmap of query string parameter name to password store name to be sent with the request */ -HttpClient.prototype.initiateHttpRequest = function(options) { +function HttpClientRequest(options) { + var self = this; console.log("Initiating an HTTP request",options) + this.wiki = options.wiki; + this.completionActions = options.oncompletion; + this.progressActions = options.onprogress; + this.bindStatus = options["bind-status"]; + this.bindProgress = options["bind-progress"]; + this.method = options.method || "GET"; + this.body = options.body || ""; + this.variables = options.variables; + var url = options.url; + $tw.utils.each(options.queryStrings,function(value,name) { + url = $tw.utils.setQueryStringParameter(url,name,value); + }); + $tw.utils.each(options.passwordQueryStrings,function(value,name) { + url = $tw.utils.setQueryStringParameter(url,name,$tw.utils.getPassword(value) || ""); + }); + this.url = url; + this.requestHeaders = {}; + $tw.utils.each(options.headers,function(value,name) { + self.requestHeaders[name] = value; + }); + $tw.utils.each(options.passwordHeaders,function(value,name) { + self.requestHeaders[name] = $tw.utils.getPassword(value) || ""; + }); +} + +HttpClientRequest.prototype.send = function() { var self = this, - wiki = options.wiki, - url = options.url, - completionActions = options.oncompletion, - progressActions = options.onprogress, - bindStatus = options["bind-status"], - bindProgress = options["bind-progress"], - method = options.method || "GET", - requestHeaders = {}, setBinding = function(title,text) { if(title) { - wiki.addTiddler(new $tw.Tiddler({title: title, text: text})); + this.wiki.addTiddler(new $tw.Tiddler({title: title, text: text})); } }; - if(url) { - setBinding(bindStatus,"pending"); - setBinding(bindProgress,"0"); - $tw.utils.each(options.queryStrings,function(value,name) { - url = $tw.utils.setQueryStringParameter(url,name,value); - }); - $tw.utils.each(options.passwordQueryStrings,function(value,name) { - url = $tw.utils.setQueryStringParameter(url,name,$tw.utils.getPassword(value) || ""); - }); - $tw.utils.each(options.headers,function(value,name) { - requestHeaders[name] = value; - }); - $tw.utils.each(options.passwordHeaders,function(value,name) { - requestHeaders[name] = $tw.utils.getPassword(value) || ""; - }); + if(this.url) { + setBinding(this.bindStatus,"pending"); + setBinding(this.bindProgress,"0"); // Set the request tracker tiddler - var requestTrackerTitle = wiki.generateNewTitle("$:/temp/HttpRequest"); - wiki.addTiddler({ + var requestTrackerTitle = this.wiki.generateNewTitle("$:/temp/HttpRequest"); + this.wiki.addTiddler({ title: requestTrackerTitle, tags: "$:/tags/HttpRequest", text: JSON.stringify({ - url: url, - type: method, + url: this.url, + type: this.method, status: "inprogress", - headers: requestHeaders, - data: options.body + headers: this.requestHeaders, + data: this.body }) }); - $tw.utils.httpRequest({ - url: url, - type: method, - headers: requestHeaders, - data: options.body, + this.xhr = $tw.utils.httpRequest({ + url: this.url, + type: this.method, + headers: this.requestHeaders, + data: this.body, callback: function(err,data,xhr) { var success = (xhr.status >= 200 && xhr.status < 300) ? "complete" : "error", headers = {}; @@ -93,8 +132,8 @@ HttpClient.prototype.initiateHttpRequest = function(options) { headers[line.substr(0,pos)] = line.substr(pos + 1).trim(); } }); - setBinding(bindStatus,success); - setBinding(bindProgress,"100"); + setBinding(self.bindStatus,success); + setBinding(self.bindProgress,"100"); var resultVariables = { status: xhr.status.toString(), statusText: xhr.statusText, @@ -103,17 +142,17 @@ HttpClient.prototype.initiateHttpRequest = function(options) { headers: JSON.stringify(headers) }; // Update the request tracker tiddler - wiki.addTiddler(new $tw.Tiddler(wiki.getTiddler(requestTrackerTitle),{ + self.wiki.addTiddler(new $tw.Tiddler(self.wiki.getTiddler(requestTrackerTitle),{ status: success, })); - wiki.invokeActionString(completionActions,undefined,$tw.utils.extend({},options.variables,resultVariables),{parentWidget: $tw.rootWidget}); + self.wiki.invokeActionString(self.completionActions,undefined,$tw.utils.extend({},self.variables,resultVariables),{parentWidget: $tw.rootWidget}); // console.log("Back!",err,data,xhr); }, progress: function(lengthComputable,loaded,total) { if(lengthComputable) { setBinding(bindProgress,"" + Math.floor((loaded/total) * 100)) } - wiki.invokeActionString(progressActions,undefined,{ + self.wiki.invokeActionString(self.progressActions,undefined,{ lengthComputable: lengthComputable ? "yes" : "no", loaded: loaded, total: total @@ -123,6 +162,12 @@ HttpClient.prototype.initiateHttpRequest = function(options) { } }; +HttpClientRequest.prototype.cancel = function() { + if(this.xhr) { + this.xhr.abort(); + } +}; + exports.HttpClient = HttpClient; /* diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid index 9419f526d..3c1030b28 100644 --- a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid @@ -88,6 +88,10 @@ https://api.zotero.org/groups/{{$:/config/zotero-group}}/items/ Start import from Zotero group +<$button message="tm-http-cancel-all-requests"> +Cancel all HTTP requests + + <$list filter="[tag[$:/tags/ZoteroImport]limit[1]]" variable="ignore"> !! Imported Tiddlers From fc22df908de55f85f4ff7264e6611bc00f5c5caa Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Tue, 2 May 2023 11:37:37 +0100 Subject: [PATCH 4/6] Make the number of outstanding HTTP requests available in a state tiddler --- core/modules/utils/dom/http.js | 59 ++++++++++++++----- ...etMessage_ tm-http-cancel-all-requests.tid | 12 ++++ ...essage_ tm-http-request Example Zotero.tid | 3 +- .../WidgetMessage_ tm-http-request.tid | 2 + 4 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-cancel-all-requests.tid diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js index 14270eef7..d16ea5208 100644 --- a/core/modules/utils/dom/http.js +++ b/core/modules/utils/dom/http.js @@ -13,20 +13,53 @@ HTTP support "use strict"; /* -Manage tm-http-request events +Manage tm-http-request events. Options include: +wiki: Reference to the wiki to be used for state tiddler tracking +stateTrackerTitle: Title of tiddler to be used for state tiddler tracking */ function HttpClient(options) { options = options || {}; this.nextId = 1; + this.wiki = options.wiki || $tw.wiki; + this.stateTrackerTitle = options.stateTrackerTitle || "$:/state/http-requests"; this.requests = []; // Array of {id: string,request: HttpClientRequest} + this.updateRequestTracker(); } +/* +Return the index into this.requests[] corresponding to a given ID. Returns null if not found +*/ +HttpClient.prototype.getRequestIndex = function(targetId) { + var targetIndex = null; + $tw.utils.each(this.requests,function(requestInfo,index) { + if(requestInfo.id === targetId) { + targetIndex = index; + } + }); + return targetIndex; +}; + +/* +Update the state tiddler that is tracking the outstanding requests +*/ +HttpClient.prototype.updateRequestTracker = function() { + this.wiki.addTiddler({title: this.stateTrackerTitle, text: "" + this.requests.length}); +}; + HttpClient.prototype.initiateHttpRequest = function(options) { - var id = this.nextId, + var self = this, + id = this.nextId, request = new HttpClientRequest(options); this.nextId += 1; this.requests.push({id: id, request: request}); - request.send(); + this.updateRequestTracker(); + request.send(function(err) { + var targetIndex = self.getRequestIndex(id); + if(targetIndex !== null) { + self.requests.splice(targetIndex,1); + self.updateRequestTracker(); + } + }); return id; }; @@ -36,18 +69,15 @@ HttpClient.prototype.cancelAllHttpRequests = function() { requestInfo.request.cancel(); }); this.requests = []; + this.updateRequestTracker(); }; HttpClient.prototype.cancelHttpRequest = function(targetId) { - var targetIndex = null; - $tw.utils.each(this.requests,function(requestInfo,index) { - if(requestInfo.id === targetId) { - targetIndex = index; - } - }); + var targetIndex = this.getRequestIndex(targetId); if(targetIndex !== null) { this.requests[targetIndex].request.cancel(); this.requests.splice(targetIndex,1); + this.updateRequestTracker(); } }; @@ -95,7 +125,7 @@ function HttpClientRequest(options) { }); } -HttpClientRequest.prototype.send = function() { +HttpClientRequest.prototype.send = function(callback) { var self = this, setBinding = function(title,text) { if(title) { @@ -124,7 +154,8 @@ HttpClientRequest.prototype.send = function() { headers: this.requestHeaders, data: this.body, callback: function(err,data,xhr) { - var success = (xhr.status >= 200 && xhr.status < 300) ? "complete" : "error", + var hasSucceeded = xhr.status >= 200 && xhr.status < 300, + completionCode = hasSucceeded ? "complete" : "error", headers = {}; $tw.utils.each(xhr.getAllResponseHeaders().split("\r\n"),function(line) { var pos = line.indexOf(":"); @@ -132,7 +163,7 @@ HttpClientRequest.prototype.send = function() { headers[line.substr(0,pos)] = line.substr(pos + 1).trim(); } }); - setBinding(self.bindStatus,success); + setBinding(self.bindStatus,completionCode); setBinding(self.bindProgress,"100"); var resultVariables = { status: xhr.status.toString(), @@ -141,11 +172,11 @@ HttpClientRequest.prototype.send = function() { data: (data || "").toString(), headers: JSON.stringify(headers) }; - // Update the request tracker tiddler self.wiki.addTiddler(new $tw.Tiddler(self.wiki.getTiddler(requestTrackerTitle),{ - status: success, + status: completionCode, })); self.wiki.invokeActionString(self.completionActions,undefined,$tw.utils.extend({},self.variables,resultVariables),{parentWidget: $tw.rootWidget}); + callback(hasSucceeded ? null : xhr.statusText); // console.log("Back!",err,data,xhr); }, progress: function(lengthComputable,loaded,total) { diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-cancel-all-requests.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-cancel-all-requests.tid new file mode 100644 index 000000000..df94e5a0b --- /dev/null +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-cancel-all-requests.tid @@ -0,0 +1,12 @@ +caption: tm-http-cancel-all-requests +created: 20230429161453032 +modified: 20230429161453032 +tags: Messages +title: WidgetMessage: tm-http-cancel-all-requests +type: text/vnd.tiddlywiki + +The ''tm-http-cancel-all-requests'' message is used to cancel all outstanding HTTP requests initiated with [[WidgetMessage: tm-http-request]]. + +Note that the state tiddler $:/state/http-requests contains a number representing the number of outstanding HTTP requests in progress. + +It does not take any parameters. diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid index 3c1030b28..ea64dd3a2 100644 --- a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request Example Zotero.tid @@ -81,7 +81,6 @@ https://api.zotero.org/groups/{{$:/config/zotero-group}}/items/ <$macrocall $name="zotero-get-items" start="0" limit="50"/> \end - <> <$button actions=<>> @@ -90,7 +89,7 @@ Start import from Zotero group <$button message="tm-http-cancel-all-requests"> Cancel all HTTP requests - + Outstanding requests: {{$:/state/http-requests}} <$list filter="[tag[$:/tags/ZoteroImport]limit[1]]" variable="ignore"> diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid index f74fdfb55..f6c82e760 100644 --- a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid @@ -44,6 +44,8 @@ The following variables are passed to the progress handler: |loaded |Number of bytes loaded so far | |total |Total number bytes to be loaded | +Note that the state tiddler $:/state/http-requests contains a number representing the number of outstanding HTTP requests in progress. + !! Examples * [[Zotero's|https://www.zotero.org/]] API for retrieving reference items: [[WidgetMessage: tm-http-request Example - Zotero]] From f798bf5611ccf430004da876d3c5e5c79cd91989 Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Tue, 2 May 2023 17:07:16 +0100 Subject: [PATCH 5/6] Add a network activity button Click it to cancel outstanding requests --- core/images/network-activity.tid | 11 +++++++++++ core/language/en-GB/Buttons.multids | 2 ++ core/palettes/Vanilla.tid | 1 + core/ui/PageControls/network-activity.tid | 16 ++++++++++++++++ core/wiki/config/PageControlButtons.multids | 1 + core/wiki/tags/PageControls.tid | 2 +- themes/tiddlywiki/vanilla/base.tid | 4 ++++ 7 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 core/images/network-activity.tid create mode 100644 core/ui/PageControls/network-activity.tid diff --git a/core/images/network-activity.tid b/core/images/network-activity.tid new file mode 100644 index 000000000..2efdfd4d4 --- /dev/null +++ b/core/images/network-activity.tid @@ -0,0 +1,11 @@ +title: $:/core/images/network-activity +tags: $:/tags/Image + + +<$list filter="[{$:/state/http-requests}match[0]]" variable="ignore"> + + +<$list filter="[{$:/state/http-requests}!match[0]]" variable="ignore"> + + + \ No newline at end of file diff --git a/core/language/en-GB/Buttons.multids b/core/language/en-GB/Buttons.multids index 85a71ac08..fa769d117 100644 --- a/core/language/en-GB/Buttons.multids +++ b/core/language/en-GB/Buttons.multids @@ -67,6 +67,8 @@ More/Caption: more More/Hint: More actions NewHere/Caption: new here NewHere/Hint: Create a new tiddler tagged with this one +NetworkActivity/Caption: network activity +NetworkActivity/Hint: Cancel all network activity NewJournal/Caption: new journal NewJournal/Hint: Create a new journal tiddler NewJournalHere/Caption: new journal here diff --git a/core/palettes/Vanilla.tid b/core/palettes/Vanilla.tid index d84b4ec83..4c660e912 100644 --- a/core/palettes/Vanilla.tid +++ b/core/palettes/Vanilla.tid @@ -54,6 +54,7 @@ modal-footer-background: #f5f5f5 modal-footer-border: #dddddd modal-header-border: #eeeeee muted-foreground: #bbb +network-activity-foreground: #448844 notification-background: #ffffdd notification-border: #999999 page-background: #f4f4f4 diff --git a/core/ui/PageControls/network-activity.tid b/core/ui/PageControls/network-activity.tid new file mode 100644 index 000000000..763365f37 --- /dev/null +++ b/core/ui/PageControls/network-activity.tid @@ -0,0 +1,16 @@ +title: $:/core/ui/Buttons/network-activity +tags: $:/tags/PageControls +caption: {{$:/core/images/network-activity}} {{$:/language/Buttons/NetworkActivity/Caption}} +description: {{$:/language/Buttons/NetworkActivity/Hint}} + +\whitespace trim +<$button message="tm-http-cancel-all-requests" tooltip={{$:/language/Buttons/NetworkActivity/Hint}} aria-label={{$:/language/Buttons/NetworkActivity/Caption}} class=<>> +<$list filter="[match[yes]]"> +{{$:/core/images/network-activity}} + +<$list filter="[match[yes]]"> + +<$text text={{$:/language/Buttons/NetworkActivity/Caption}}/> + + + \ No newline at end of file diff --git a/core/wiki/config/PageControlButtons.multids b/core/wiki/config/PageControlButtons.multids index a437251f5..b66f11cc0 100644 --- a/core/wiki/config/PageControlButtons.multids +++ b/core/wiki/config/PageControlButtons.multids @@ -13,6 +13,7 @@ core/ui/Buttons/language: hide core/ui/Buttons/tag-manager: hide core/ui/Buttons/manager: hide core/ui/Buttons/more-page-actions: hide +core/ui/Buttons/network-activity: hide core/ui/Buttons/new-journal: hide core/ui/Buttons/new-image: hide core/ui/Buttons/palette: hide diff --git a/core/wiki/tags/PageControls.tid b/core/wiki/tags/PageControls.tid index c6234751c..c0f1cb233 100644 --- a/core/wiki/tags/PageControls.tid +++ b/core/wiki/tags/PageControls.tid @@ -1,2 +1,2 @@ title: $:/tags/PageControls -list: [[$:/core/ui/Buttons/home]] [[$:/core/ui/Buttons/close-all]] [[$:/core/ui/Buttons/fold-all]] [[$:/core/ui/Buttons/unfold-all]] [[$:/core/ui/Buttons/permaview]] [[$:/core/ui/Buttons/new-tiddler]] [[$:/core/ui/Buttons/new-journal]] [[$:/core/ui/Buttons/new-image]] [[$:/core/ui/Buttons/import]] [[$:/core/ui/Buttons/export-page]] [[$:/core/ui/Buttons/control-panel]] [[$:/core/ui/Buttons/advanced-search]] [[$:/core/ui/Buttons/manager]] [[$:/core/ui/Buttons/tag-manager]] [[$:/core/ui/Buttons/language]] [[$:/core/ui/Buttons/palette]] [[$:/core/ui/Buttons/theme]] [[$:/core/ui/Buttons/layout]] [[$:/core/ui/Buttons/storyview]] [[$:/core/ui/Buttons/encryption]] [[$:/core/ui/Buttons/timestamp]] [[$:/core/ui/Buttons/full-screen]] [[$:/core/ui/Buttons/print]] [[$:/core/ui/Buttons/save-wiki]] [[$:/core/ui/Buttons/refresh]] [[$:/core/ui/Buttons/more-page-actions]] +list: [[$:/core/ui/Buttons/home]] [[$:/core/ui/Buttons/close-all]] [[$:/core/ui/Buttons/fold-all]] [[$:/core/ui/Buttons/unfold-all]] [[$:/core/ui/Buttons/permaview]] [[$:/core/ui/Buttons/new-tiddler]] [[$:/core/ui/Buttons/new-journal]] [[$:/core/ui/Buttons/new-image]] [[$:/core/ui/Buttons/import]] [[$:/core/ui/Buttons/export-page]] [[$:/core/ui/Buttons/control-panel]] [[$:/core/ui/Buttons/advanced-search]] [[$:/core/ui/Buttons/manager]] [[$:/core/ui/Buttons/tag-manager]] [[$:/core/ui/Buttons/language]] [[$:/core/ui/Buttons/palette]] [[$:/core/ui/Buttons/theme]] [[$:/core/ui/Buttons/layout]] [[$:/core/ui/Buttons/storyview]] [[$:/core/ui/Buttons/encryption]] [[$:/core/ui/Buttons/timestamp]] [[$:/core/ui/Buttons/full-screen]] [[$:/core/ui/Buttons/print]] [[$:/core/ui/Buttons/save-wiki]] [[$:/core/ui/Buttons/refresh]] [[$:/core/ui/Buttons/network-activity]] [[$:/core/ui/Buttons/more-page-actions]] diff --git a/themes/tiddlywiki/vanilla/base.tid b/themes/tiddlywiki/vanilla/base.tid index f63384ee9..1ba6a89b2 100644 --- a/themes/tiddlywiki/vanilla/base.tid +++ b/themes/tiddlywiki/vanilla/base.tid @@ -3156,6 +3156,10 @@ select { fill: <>; } +.tc-network-activity-background { + fill: <>; +} + /* ** Flexbox utility classes */ From 499eafcd34191ae2f5dd4e6312a3575c09d0bce5 Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Tue, 2 May 2023 17:15:14 +0100 Subject: [PATCH 6/6] WIP --- core/modules/commands/setfield.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/modules/commands/setfield.js b/core/modules/commands/setfield.js index 3f8ec1d14..620aa47ee 100644 --- a/core/modules/commands/setfield.js +++ b/core/modules/commands/setfield.js @@ -39,9 +39,10 @@ Command.prototype.execute = function() { $tw.utils.each(tiddlers,function(title) { var parser = wiki.parseTiddler(templatetitle), newFields = {}, - tiddler = wiki.getTiddler(title); + tiddler = wiki.getTiddler(title), + currentValue = tiddler ? (tiddler.fields[fieldname] || "") : ""; if(parser) { - var widgetNode = wiki.makeWidget(parser,{variables: {currentTiddler: title}}); + var widgetNode = wiki.makeWidget(parser,{variables: {currentTiddler: title, currentValue: currentValue}}); var container = $tw.fakeDocument.createElement("div"); widgetNode.render(container,null); newFields[fieldname] = rendertype === "text/html" ? container.innerHTML : container.textContent;