diff --git a/core/language/en-GB/Fields.multids b/core/language/en-GB/Fields.multids index 6b4823c02f9..d1eeabd4295 100644 --- a/core/language/en-GB/Fields.multids +++ b/core/language/en-GB/Fields.multids @@ -35,3 +35,4 @@ title: The unique name of a tiddler toc-link: Suppresses the tiddler's link in a Table of Contents tree if set to: ''no'' type: The content type of a tiddler version: Version information for a plugin +_is_skinny: If present, indicates that the tiddler text field must be loaded from the server diff --git a/core/language/en-GB/Misc.multids b/core/language/en-GB/Misc.multids index ee1d0320ef5..861bb68995c 100644 --- a/core/language/en-GB/Misc.multids +++ b/core/language/en-GB/Misc.multids @@ -28,6 +28,7 @@ Error/Filter: Filter error Error/FilterSyntax: Syntax error in filter expression Error/IsFilterOperator: Filter Error: Unknown operand for the 'is' filter operator Error/LoadingPluginLibrary: Error loading plugin library +Error/NetworkErrorAlert: `

''Network Error''

It looks like the connection to the server has been lost. This may indicate a problem with your network connection. Please attempt to restore network connectivity before continuing.

''Any unsaved changes will be automatically synchronised when connectivity is restored''.` Error/RecursiveTransclusion: Recursive transclusion error in transclude widget Error/RetrievingSkinny: Error retrieving skinny tiddler list Error/SavingToTWEdit: Error saving to TWEdit diff --git a/core/modules/server/routes/get-file.js b/core/modules/server/routes/get-file.js index 3429c4cd208..2a0ef647abf 100644 --- a/core/modules/server/routes/get-file.js +++ b/core/modules/server/routes/get-file.js @@ -19,22 +19,16 @@ exports.path = /^\/files\/(.+)$/; exports.handler = function(request,response,state) { var path = require("path"), fs = require("fs"), - util = require("util"); - var filename = path.resolve($tw.boot.wikiPath,"files",decodeURIComponent(state.params[0])), + util = require("util"), + suppliedFilename = decodeURIComponent(state.params[0]), + filename = path.resolve($tw.boot.wikiPath,"files",suppliedFilename), extension = path.extname(filename); fs.readFile(filename,function(err,content) { var status,content,type = "text/plain"; if(err) { - if(err.code === "ENOENT") { - status = 404; - content = "File '" + filename + "' not found"; - } else if(err.code === "EACCES") { - status = 403; - content = "You do not have permission to access the file '" + filename + "'"; - } else { - status = 500; - content = err.toString(); - } + console.log("Error accessing file " + filename + ": " + err.toString()); + status = 404; + content = "File '" + suppliedFilename + "' not found"; } else { status = 200; content = content; diff --git a/core/modules/server/routes/get-tiddlers-json.js b/core/modules/server/routes/get-tiddlers-json.js index 3ece35ce13b..8e93733e48d 100644 --- a/core/modules/server/routes/get-tiddlers-json.js +++ b/core/modules/server/routes/get-tiddlers-json.js @@ -3,7 +3,7 @@ title: $:/core/modules/server/routes/get-tiddlers-json.js type: application/javascript module-type: route -GET /recipes/default/tiddlers/tiddlers.json +GET /recipes/default/tiddlers/tiddlers.json?filter= \*/ (function() { @@ -12,23 +12,34 @@ GET /recipes/default/tiddlers/tiddlers.json /*global $tw: false */ "use strict"; +var DEFAULT_FILTER = "[all[tiddlers]!is[system]sort[title]]"; + exports.method = "GET"; exports.path = /^\/recipes\/default\/tiddlers.json$/; exports.handler = function(request,response,state) { + var filter = state.queryParameters.filter || DEFAULT_FILTER; + if($tw.wiki.getTiddlerText("$:/config/Server/AllowAllExternalFilters") !== "yes") { + if($tw.wiki.getTiddlerText("$:/config/Server/ExternalFilters/" + filter) !== "yes") { + console.log("Blocked attempt to GET /recipes/default/tiddlers/tiddlers.json with filter: " + filter); + response.writeHead(403); + response.end(); + return; + } + } + var excludeFields = (state.queryParameters.exclude || "text").split(","), + titles = state.wiki.filterTiddlers(filter); response.writeHead(200, {"Content-Type": "application/json"}); var tiddlers = []; - state.wiki.forEachTiddler({sortField: "title"},function(title,tiddler) { - var tiddlerFields = {}; - $tw.utils.each(tiddler.fields,function(field,name) { - if(name !== "text") { - tiddlerFields[name] = tiddler.getFieldString(name); - } - }); - tiddlerFields.revision = state.wiki.getChangeCount(title); - tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; - tiddlers.push(tiddlerFields); + $tw.utils.each(titles,function(title) { + var tiddler = state.wiki.getTiddler(title); + if(tiddler) { + var tiddlerFields = tiddler.getFieldStrings({exclude: excludeFields}); + tiddlerFields.revision = state.wiki.getChangeCount(title); + tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki"; + tiddlers.push(tiddlerFields); + } }); var text = JSON.stringify(tiddlers); response.end(text,"utf8"); diff --git a/core/modules/server/server.js b/core/modules/server/server.js index 3225b95f39d..3226cacd727 100644 --- a/core/modules/server/server.js +++ b/core/modules/server/server.js @@ -16,7 +16,8 @@ if($tw.node) { var util = require("util"), fs = require("fs"), url = require("url"), - path = require("path"); + path = require("path"), + querystring = require("querystring"); } /* @@ -162,6 +163,7 @@ Server.prototype.requestHandler = function(request,response) { state.wiki = self.wiki; state.server = self; state.urlInfo = url.parse(request.url); + state.queryParameters = querystring.parse(state.urlInfo.query); // Get the principals authorized to access this resource var authorizationType = this.methodMappings[request.method] || "readers"; // Check for the CSRF header if this is a write @@ -236,6 +238,7 @@ host: optional host address (falls back to value of "host" variable) prefix: optional prefix (falls back to value of "path-prefix" variable) */ Server.prototype.listen = function(port,host,prefix) { + var self = this; // Handle defaults for port and host port = port || this.get("port"); host = host || this.get("host"); @@ -244,19 +247,24 @@ Server.prototype.listen = function(port,host,prefix) { if(parseInt(port,10).toString() !== port) { port = process.env[port] || 8080; } - $tw.utils.log("Serving on " + this.protocol + "://" + host + ":" + port + prefix,"brown/orange"); - $tw.utils.log("(press ctrl-C to exit)","red"); // Warn if required plugins are missing if(!$tw.wiki.getTiddler("$:/plugins/tiddlywiki/tiddlyweb") || !$tw.wiki.getTiddler("$:/plugins/tiddlywiki/filesystem")) { $tw.utils.warning("Warning: Plugins required for client-server operation (\"tiddlywiki/filesystem\" and \"tiddlywiki/tiddlyweb\") are missing from tiddlywiki.info file"); } - // Listen + // Create the server var server; if(this.listenOptions) { server = this.transport.createServer(this.listenOptions,this.requestHandler.bind(this)); } else { server = this.transport.createServer(this.requestHandler.bind(this)); } + // Display the port number after we've started listening (the port number might have been specified as zero, in which case we will get an assigned port) + server.on("listening",function() { + var address = server.address(); + $tw.utils.log("Serving on " + self.protocol + "://" + address.address + ":" + address.port + prefix,"brown/orange"); + $tw.utils.log("(press ctrl-C to exit)","red"); + }); + // Listen return server.listen(port,host); }; diff --git a/core/modules/startup/startup.js b/core/modules/startup/startup.js index 4cd53dfcded..ad1416bf39c 100755 --- a/core/modules/startup/startup.js +++ b/core/modules/startup/startup.js @@ -128,7 +128,7 @@ exports.startup = function() { // Set up the syncer object if we've got a syncadaptor if($tw.syncadaptor) { $tw.syncer = new $tw.Syncer({wiki: $tw.wiki, syncadaptor: $tw.syncadaptor}); - } + } // Setup the saver handler $tw.saverHandler = new $tw.SaverHandler({ wiki: $tw.wiki, diff --git a/core/modules/syncer.js b/core/modules/syncer.js index 0b84be750ea..f39646eace7 100644 --- a/core/modules/syncer.js +++ b/core/modules/syncer.js @@ -3,7 +3,7 @@ title: $:/core/modules/syncer.js type: application/javascript module-type: global -The syncer tracks changes to the store. If a syncadaptor is used then individual tiddlers are synchronised through it. If there is no syncadaptor then the entire wiki is saved via saver modules. +The syncer tracks changes to the store and synchronises them to a remote data store represented as a "sync adaptor" \*/ (function(){ @@ -23,8 +23,10 @@ Syncer.prototype.titleSyncFilter = "$:/config/SyncFilter"; Syncer.prototype.titleSyncPollingInterval = "$:/config/SyncPollingInterval"; Syncer.prototype.titleSyncDisableLazyLoading = "$:/config/SyncDisableLazyLoading"; Syncer.prototype.titleSavedNotification = "$:/language/Notifications/Save/Done"; +Syncer.prototype.titleSyncThrottleInterval = "$:/config/SyncThrottleInterval"; Syncer.prototype.taskTimerInterval = 1 * 1000; // Interval for sync timer Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s... +Syncer.prototype.errorRetryInterval = 5 * 1000; // Interval to retry after an error Syncer.prototype.fallbackInterval = 10 * 1000; // Unless the task is older than 10s Syncer.prototype.pollTimerInterval = 60 * 1000; // Interval for polling for changes from the adaptor @@ -36,6 +38,7 @@ wiki: wiki to be synced function Syncer(options) { var self = this; this.wiki = options.wiki; + // Save parameters this.syncadaptor = options.syncadaptor; this.disableUI = !!options.disableUI; this.titleIsLoggedIn = options.titleIsLoggedIn || this.titleIsLoggedIn; @@ -43,27 +46,54 @@ function Syncer(options) { this.titleSyncFilter = options.titleSyncFilter || this.titleSyncFilter; this.titleSavedNotification = options.titleSavedNotification || this.titleSavedNotification; this.taskTimerInterval = options.taskTimerInterval || this.taskTimerInterval; - this.throttleInterval = options.throttleInterval || this.throttleInterval; + this.throttleInterval = options.throttleInterval || parseInt(this.wiki.getTiddlerText(this.titleSyncThrottleInterval,""),10) || this.throttleInterval; + this.errorRetryInterval = options.errorRetryInterval || this.errorRetryInterval; this.fallbackInterval = options.fallbackInterval || this.fallbackInterval; this.pollTimerInterval = options.pollTimerInterval || parseInt(this.wiki.getTiddlerText(this.titleSyncPollingInterval,""),10) || this.pollTimerInterval; this.logging = "logging" in options ? options.logging : true; // Make a logger this.logger = new $tw.utils.Logger("syncer" + ($tw.browser ? "-browser" : "") + ($tw.node ? "-server" : "") + (this.syncadaptor.name ? ("-" + this.syncadaptor.name) : ""),{ - colour: "cyan", - enable: this.logging - }); + colour: "cyan", + enable: this.logging, + saveHistory: true + }); + if(this.syncadaptor.setLoggerSaveBuffer) { + this.syncadaptor.setLoggerSaveBuffer(this.logger); + } // Compile the dirty tiddler filter this.filterFn = this.wiki.compileFilter(this.wiki.getTiddlerText(this.titleSyncFilter)); // Record information for known tiddlers this.readTiddlerInfo(); - // Tasks are {type: "load"/"save"/"delete", title:, queueTime:, lastModificationTime:} - this.taskQueue = {}; // Hashmap of tasks yet to be performed - this.taskInProgress = {}; // Hash of tasks in progress + this.titlesToBeLoaded = {}; // Hashmap of titles of tiddlers that need loading from the server + this.titlesHaveBeenLazyLoaded = {}; // Hashmap of titles of tiddlers that have already been lazily loaded from the server + // Timers this.taskTimerId = null; // Timer for task dispatch this.pollTimerId = null; // Timer for polling server + // Number of outstanding requests + this.numTasksInProgress = 0; // Listen out for changes to tiddlers this.wiki.addEventListener("change",function(changes) { - self.syncToServer(changes); + // Filter the changes to just include ones that are being synced + var filteredChanges = self.getSyncedTiddlers(function(callback) { + $tw.utils.each(changes,function(change,title) { + var tiddler = self.wiki.tiddlerExists(title) && self.wiki.getTiddler(title); + callback(tiddler,title); + }); + }); + if(filteredChanges.length > 0) { + self.processTaskQueue(); + } else { + // Look for deletions of tiddlers we're already syncing + var outstandingDeletion = false + $tw.utils.each(changes,function(change,title,object) { + if(change.deleted && $tw.utils.hop(self.tiddlerInfo,title)) { + outstandingDeletion = true; + } + }); + if(outstandingDeletion) { + self.processTaskQueue(); + } + } }); // Browser event handlers if($tw.browser && !this.disableUI) { @@ -86,6 +116,9 @@ function Syncer(options) { $tw.rootWidget.addEventListener("tm-server-refresh",function() { self.handleRefreshEvent(); }); + $tw.rootWidget.addEventListener("tm-copy-syncer-logs-to-clipboard",function() { + $tw.utils.copyToClipboard($tw.utils.getSystemInfo() + "\n\nLog:\n" + self.logger.getBuffer()); + }); } // Listen out for lazyLoad events if(!this.disableUI && $tw.wiki.getTiddlerText(this.titleSyncDisableLazyLoading) !== "yes") { @@ -100,45 +133,83 @@ function Syncer(options) { }); } +/* +Show a generic network error alert +*/ +Syncer.prototype.showErrorAlert = function() { +console.log($tw.language.getString("Error/NetworkErrorAlert")) + this.logger.alert($tw.language.getString("Error/NetworkErrorAlert")); +}; + +/* +Return an array of the tiddler titles that are subjected to syncing +*/ +Syncer.prototype.getSyncedTiddlers = function(source) { + return this.filterFn.call(this.wiki,source); +}; + +/* +Return an array of the tiddler titles that are subjected to syncing +*/ +Syncer.prototype.getTiddlerRevision = function(title) { + if(this.syncadaptor && this.syncadaptor.getTiddlerRevision) { + return this.syncadaptor.getTiddlerRevision(title); + } else { + return this.wiki.getTiddler(title).fields.revision; + } +}; + /* Read (or re-read) the latest tiddler info from the store */ Syncer.prototype.readTiddlerInfo = function() { // Hashmap by title of {revision:,changeCount:,adaptorInfo:} + // "revision" is the revision of the tiddler last seen on the server, and "changecount" is the corresponding local changecount this.tiddlerInfo = {}; // Record information for known tiddlers var self = this, - tiddlers = this.filterFn.call(this.wiki); + tiddlers = this.getSyncedTiddlers(); $tw.utils.each(tiddlers,function(title) { - var tiddler = self.wiki.getTiddler(title); + var tiddler = self.wiki.tiddlerExists(title) && self.wiki.getTiddler(title); self.tiddlerInfo[title] = { - revision: tiddler.fields.revision, + revision: self.getTiddlerRevision(title), adaptorInfo: self.syncadaptor && self.syncadaptor.getTiddlerInfo(tiddler), - changeCount: self.wiki.getChangeCount(title), - hasBeenLazyLoaded: false + changeCount: self.wiki.getChangeCount(title) }; }); }; -/* -Create an tiddlerInfo structure if it doesn't already exist -*/ -Syncer.prototype.createTiddlerInfo = function(title) { - if(!$tw.utils.hop(this.tiddlerInfo,title)) { - this.tiddlerInfo[title] = { - revision: null, - adaptorInfo: {}, - changeCount: -1, - hasBeenLazyLoaded: false - }; - } -}; - /* Checks whether the wiki is dirty (ie the window shouldn't be closed) */ Syncer.prototype.isDirty = function() { - return (this.numTasksInQueue() > 0) || (this.numTasksInProgress() > 0); + this.logger.log("Checking dirty status"); + // Check tiddlers that are in the store and included in the filter function + var titles = this.getSyncedTiddlers(); + for(var index=0; index tiddlerInfo.changeCount) { + return true; + } + } else { + // If the tiddler isn't known on the server then it needs to be saved to the server + return true; + } + } + } + // Check tiddlers that are known from the server but not currently in the store + titles = Object.keys(this.tiddlerInfo); + for(index=0; index 0 || updates.deletions.length > 0) { + self.processTaskQueue(); + } + } + }); + } else if(this.syncadaptor && this.syncadaptor.getSkinnyTiddlers) { + this.logger.log("Retrieving skinny tiddler list"); + cancelNextSync(); + this.syncadaptor.getSkinnyTiddlers(function(err,tiddlers) { + triggerNextSync(); // Check for errors if(err) { - self.logger.alert($tw.language.getString("Error/RetrievingSkinny") + ":",err); + self.showErrorAlert(); + self.logger.log($tw.language.getString("Error/RetrievingSkinny") + ":",err); return; } + // Keep track of which tiddlers we already know about have been reported this time + var previousTitles = Object.keys(self.tiddlerInfo); // Process each incoming tiddler for(var t=0; t tiddlerInfo.changeCount, + isReadyToSave = !tiddlerInfo || !tiddlerInfo.timestampLastSaved || tiddlerInfo.timestampLastSaved < thresholdLastSaved; + if(hasChanged) { + if(isReadyToSave) { + return new SaveTiddlerTask(this,title); + } else { + havePending = true; } - // Mark that this task is no longer in progress - delete self.taskInProgress[task.title]; - self.updateDirtyStatus(); - // Process the next task - self.processTaskQueue.call(self); - }); - } else { - // Make sure we've set a time if there wasn't a task to perform, but we've still got tasks in the queue - if(this.numTasksInQueue() > 0) { - this.triggerTimeout(); } } } -}; - -/* -Choose the next applicable task -*/ -Syncer.prototype.chooseNextTask = function() { - var self = this, - candidateTask = null, - now = Date.now(); - // Select the best candidate task - $tw.utils.each(this.taskQueue,function(task,title) { - // Exclude the task if there's one of the same name in progress - if($tw.utils.hop(self.taskInProgress,title)) { - return; - } - // Exclude the task if it is a save and the tiddler has been modified recently, but not hit the fallback time - if(task.type === "save" && (now - task.lastModificationTime) < self.throttleInterval && - (now - task.queueTime) < self.fallbackInterval) { - return; - } - // Exclude the task if it is newer than the current best candidate - if(candidateTask && candidateTask.queueTime < task.queueTime) { - return; + // Second, we check tiddlers that are known from the server but not currently in the store, and so need deleting on the server + titles = Object.keys(this.tiddlerInfo); + for(index=0; index 1) { @@ -87,6 +115,22 @@ Logger.prototype.alert = function(/* args */) { } }; +/* +Clear outstanding alerts +*/ +Logger.prototype.clearAlerts = function() { + var self = this; + if($tw.browser && this.alertCount > 0) { + $tw.utils.each($tw.wiki.getTiddlersWithTag(ALERT_TAG),function(title) { + var tiddler = $tw.wiki.getTiddler(title); + if(tiddler.fields.component === self.componentName) { + $tw.wiki.deleteTiddler(title); + } + }); + this.alertCount = 0; + } +}; + exports.Logger = Logger; })(); diff --git a/core/modules/utils/utils.js b/core/modules/utils/utils.js index 2495be49fa5..7787783e5c9 100644 --- a/core/modules/utils/utils.js +++ b/core/modules/utils/utils.js @@ -779,4 +779,22 @@ exports.strEndsWith = function(str,ending,position) { } }; +/* +Return system information useful for debugging +*/ +exports.getSystemInfo = function(str,ending,position) { + var results = [], + save = function(desc,value) { + results.push(desc + ": " + value); + }; + if($tw.browser) { + save("User Agent",navigator.userAgent); + save("Online Status",window.navigator.onLine); + } + if($tw.node) { + save("Node Version",process.version); + } + return results.join("\n"); +}; + })(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index fc8b42eda40..2adfb0ef307 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -1233,9 +1233,9 @@ exports.getTiddlerText = function(title,defaultText) { if(!tiddler) { return defaultText; } - if(tiddler.fields.text !== undefined) { + if(!tiddler.hasField("_is_skinny")) { // Just return the text if we've got it - return tiddler.fields.text; + return tiddler.fields.text || ""; } else { // Tell any listeners about the need to lazily load this tiddler this.dispatchEvent("lazyLoad",title); diff --git a/core/templates/html-div-skinny-tiddler.tid b/core/templates/html-div-skinny-tiddler.tid new file mode 100644 index 00000000000..eaf388605e1 --- /dev/null +++ b/core/templates/html-div-skinny-tiddler.tid @@ -0,0 +1,9 @@ +title: $:/core/templates/html-div-skinny-tiddler + +``> +

+`
diff --git a/core/templates/save-lazy-all.tid b/core/templates/save-lazy-all.tid
index 5f9220e192a..bf7f9f606a8 100644
--- a/core/templates/save-lazy-all.tid
+++ b/core/templates/save-lazy-all.tid
@@ -3,4 +3,7 @@ title: $:/core/save/lazy-all
 \define saveTiddlerFilter()
 [is[system]] -[prefix[$:/state/popup/]] -[[$:/HistoryList]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] 
 \end
+\define skinnySaveTiddlerFilter()
+[!is[system]]
+\end
 {{$:/core/templates/tiddlywiki5.html}}
diff --git a/core/templates/save-lazy-images.tid b/core/templates/save-lazy-images.tid
index ff3204729c8..62334f0db32 100644
--- a/core/templates/save-lazy-images.tid
+++ b/core/templates/save-lazy-images.tid
@@ -3,4 +3,7 @@ title: $:/core/save/lazy-images
 \define saveTiddlerFilter()
 [is[tiddler]] -[prefix[$:/state/popup/]] -[[$:/HistoryList]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[!is[system]is[image]] +[sort[title]] 
 \end
+\define skinnySaveTiddlerFilter()
+[is[image]]
+\end
 {{$:/core/templates/tiddlywiki5.html}}
diff --git a/core/templates/store.area.template.html.tid b/core/templates/store.area.template.html.tid
index 3563da318c1..bd9232be369 100644
--- a/core/templates/store.area.template.html.tid
+++ b/core/templates/store.area.template.html.tid
@@ -3,6 +3,7 @@ title: $:/core/templates/store.area.template.html
 <$reveal type="nomatch" state="$:/isEncrypted" text="yes">
 ``
 
 <$reveal type="match" state="$:/isEncrypted" text="yes">
diff --git a/core/ui/AlertTemplate.tid b/core/ui/AlertTemplate.tid
index bcfc3c3fa3a..84b9632f7cf 100644
--- a/core/ui/AlertTemplate.tid
+++ b/core/ui/AlertTemplate.tid
@@ -2,10 +2,12 @@ title: $:/core/ui/AlertTemplate
 
 
-<$button class="tc-btn-invisible"><$action-deletetiddler $tiddler=<>/>{{$:/core/images/delete-button}} +<$button class="tc-btn-invisible"><$action-deletetiddler $tiddler=<>/>{{$:/core/images/cancel-button}}
-<$view field="component"/> - <$view field="modified" format="date" template="0hh:0mm:0ss DD MM YYYY"/> <$reveal type="nomatch" state="!!count" text="">({{$:/language/Count}}: <$view field="count"/>) +<$wikify name="format" text=<>> +<$view field="component"/> - <$view field="modified" format="date" template=<>/> <$reveal type="nomatch" state="!!count" text="">({{$:/language/Count}}: <$view field="count"/>) +
diff --git a/core/wiki/config/ServerExternalFiltersDefault.tid b/core/wiki/config/ServerExternalFiltersDefault.tid new file mode 100644 index 00000000000..7ef93ecf377 --- /dev/null +++ b/core/wiki/config/ServerExternalFiltersDefault.tid @@ -0,0 +1,2 @@ +title: $:/config/Server/ExternalFilters/[all[tiddlers]!is[system]sort[title]] +text: yes diff --git a/editions/dev/tiddlers/from tw5.com/moduletypes/SyncAdaptorModules.tid b/editions/dev/tiddlers/from tw5.com/moduletypes/SyncAdaptorModules.tid index 6fdc6af3cfc..6f88fb9a50c 100644 --- a/editions/dev/tiddlers/from tw5.com/moduletypes/SyncAdaptorModules.tid +++ b/editions/dev/tiddlers/from tw5.com/moduletypes/SyncAdaptorModules.tid @@ -1,5 +1,5 @@ created: 20130825162100000 -modified: 20140814094907624 +modified: 20200113094126878 tags: dev moduletypes title: SyncAdaptorModules type: text/vnd.tiddlywiki @@ -14,6 +14,8 @@ SyncAdaptorModules encapsulate storage mechanisms that can be used by the SyncMe SyncAdaptorModules are represented as JavaScript tiddlers with the field `module-type` set to `syncadaptor`. +See [[this pull request|https://github.com/Jermolene/TiddlyWiki5/pull/4373]] for background on the evolution of this API. + ! Exports The following properties should be exposed via the `exports` object: @@ -47,12 +49,21 @@ Gets the supplemental information that the adaptor needs to keep track of for a Returns an object storing any additional information required by the adaptor. +!! `getTiddlerRevision(title)` + +Gets the revision ID associated with the specified tiddler title. + +|!Parameter |!Description | +|title |Tiddler title | + +Returns a revision ID. + !! `getStatus(callback)` Retrieves status information from the server. This method is optional. |!Parameter |!Description | -|callback |Callback function invoked with parameters `err,isLoggedIn,username` | +|callback |Callback function invoked with parameters `err,isLoggedIn,username,isReadOnly` | !! `login(username,password,callback)` @@ -70,16 +81,39 @@ Attempts to logout of the server. This method is optional. |!Parameter |!Description | |callback |Callback function invoked with parameter `err` | +!! `getUpdatedTiddlers(syncer,callback)` + +Retrieves the titles of tiddlers that need to be updated from the server. + +This method is optional. If an adaptor doesn't implement it then synchronisation will be unidirectional from the TiddlyWiki store to the adaptor, but not the other way. + +The syncer will use the `getUpdatedTiddlers()` method in preference to the `getSkinnyTiddlers()` method. + +|!Parameter |!Description | +|syncer |Reference to the syncer object making the call | +|callback |Callback function invoked with parameter `err,data` -- see below | + +The data provided by the callback is as follows: + +``` +{ +modifications: [], +deletions: [], +} +``` + !! `getSkinnyTiddlers(callback)` Retrieves a list of skinny tiddlers from the server. This method is optional. If an adaptor doesn't implement it then synchronisation will be unidirectional from the TiddlyWiki store to the adaptor, but not the other way. +The syncer will use the `getUpdatedTiddlers()` method in preference to the `getSkinnyTiddlers()` method. + |!Parameter |!Description | |callback |Callback function invoked with parameter `err,tiddlers`, where `tiddlers` is an array of tiddler field objects | -!! `saveTiddler(tiddler,callback,tiddlerInfo)` +!! `saveTiddler(tiddler,callback)` Saves a tiddler to the server. @@ -96,11 +130,16 @@ Loads a tiddler from the server. |title |Title of tiddler to be retrieved | |callback |Callback function invoked with parameter `err,tiddlerFields` | -!! `deleteTiddler(title,callback,tiddlerInfo)` +!! `deleteTiddler(title,callback,options)` Delete a tiddler from the server. |!Parameter |!Description | |title |Title of tiddler to be deleted | |callback |Callback function invoked with parameter `err` | +|options |See below | + +The options parameter contains the following properties: + +|!Property |!Description | |tiddlerInfo |The tiddlerInfo maintained by the syncer for this tiddler | diff --git a/editions/tw5.com/tiddlers/concepts/TiddlerFields.tid b/editions/tw5.com/tiddlers/concepts/TiddlerFields.tid index 0a0c07c7e6f..ced34345404 100644 --- a/editions/tw5.com/tiddlers/concepts/TiddlerFields.tid +++ b/editions/tw5.com/tiddlers/concepts/TiddlerFields.tid @@ -1,5 +1,5 @@ created: 20130825213300000 -modified: 20191013093910961 +modified: 20191206152031468 tags: Concepts title: TiddlerFields type: text/vnd.tiddlywiki @@ -42,11 +42,13 @@ Other fields used by the core are: |`subtitle` |<> | |`throttle.refresh` |<> | |`toc-link`|<>| +|`_canonical_uri`|<>| The TiddlyWebAdaptor uses a few more fields: |!Field Name |!Description | |`bag` |<> | |`revision` |<> | +|`_is_skinny` |<> | Details of the fields used in this ~TiddlyWiki are shown in the [[control panel|$:/ControlPanel]] {{$:/core/ui/Buttons/control-panel}} under the <<.controlpanel-tab Info>> tab >> <<.info-tab Advanced>> sub-tab >> Tiddler Fields diff --git a/editions/tw5.com/tiddlers/webserver/WebServer API_ Get All Tiddlers.tid b/editions/tw5.com/tiddlers/webserver/WebServer API_ Get All Tiddlers.tid index 98119804ca5..52ca8964f15 100644 --- a/editions/tw5.com/tiddlers/webserver/WebServer API_ Get All Tiddlers.tid +++ b/editions/tw5.com/tiddlers/webserver/WebServer API_ Get All Tiddlers.tid @@ -1,5 +1,5 @@ created: 20181002131215403 -modified: 20190903094711346 +modified: 2020031109590546 tags: [[WebServer API]] title: WebServer API: Get All Tiddlers type: text/vnd.tiddlywiki @@ -12,11 +12,23 @@ GET /recipes/default/tiddlers.json Parameters: -* none +* ''filter'' - filter identifying tiddlers to be returned (optional, defaults to "[all[tiddlers]!is[system]sort[title]]") +* ''exclude'' - comma delimited list of fields to excluded from the returned tiddlers (optional, defaults to "text") -Response: +In order to avoid denial of service attacks with malformed filters in the default configuration the only filter that is accepted is the default filter "[all[tiddlers]!is[system]sort[title]]"; attempts to use any other filter will result in an HTTP 403 error. + +To enable a particular filter, create a tiddler with the title "$:/config/Server/ExternalFilters/" concatenated with the filter text, and the text field set to "yes". For example, the TiddlyWeb plugin includes the following shadow tiddler to enable the filter that it requires: +``` +title: $:/config/Server/ExternalFilters/[all[tiddlers]] -[[$:/isEncrypted]] -[prefix[$:/temp/]] -[prefix[$:/status/]] +text: yes +``` + +It is also possible to configure the server to accept any filter by creating a tiddler titled $:/config/Server/AllowAllExternalFilters with the text "yes". This should not be done for public facing servers. + +Response: * 200 OK *> `Content-Type: application/json` *> Body: array of all non-system tiddlers in [[TiddlyWeb JSON tiddler format]] +* 403 Forbidden diff --git a/editions/tw5.com/tiddlers/webserver/WebServer Parameter_ port.tid b/editions/tw5.com/tiddlers/webserver/WebServer Parameter_ port.tid index f05da08fca3..bbfee9906a6 100644 --- a/editions/tw5.com/tiddlers/webserver/WebServer Parameter_ port.tid +++ b/editions/tw5.com/tiddlers/webserver/WebServer Parameter_ port.tid @@ -1,6 +1,6 @@ caption: port created: 20180630180552254 -modified: 20180702155017130 +modified: 20191219123751824 tags: [[WebServer Parameters]] title: WebServer Parameter: port type: text/vnd.tiddlywiki @@ -10,6 +10,7 @@ The [[web server configuration parameter|WebServer Parameters]] ''port'' specifi The ''port'' parameter accepts two types of value: * Numerical values are interpreted as a decimal port number +** The special value 0 (zero) causes the operating system to assign an available port * Non-numeric values are interpreted as an environment variable from which the port should be read This example configures the server to listen on port 8090: diff --git a/plugins/tiddlywiki/filesystem/filesystemadaptor.js b/plugins/tiddlywiki/filesystem/filesystemadaptor.js index a346a66065f..9e0734814ff 100644 --- a/plugins/tiddlywiki/filesystem/filesystemadaptor.js +++ b/plugins/tiddlywiki/filesystem/filesystemadaptor.js @@ -26,6 +26,8 @@ function FileSystemAdaptor(options) { FileSystemAdaptor.prototype.name = "filesystem"; +FileSystemAdaptor.prototype.supportsLazyLoading = false; + FileSystemAdaptor.prototype.isReady = function() { // The file system adaptor is always ready return true; diff --git a/plugins/tiddlywiki/tiddlyweb/ServerControlPanel.tid b/plugins/tiddlywiki/tiddlyweb/ServerControlPanel.tid deleted file mode 100644 index 9ef1e27afbb..00000000000 --- a/plugins/tiddlywiki/tiddlyweb/ServerControlPanel.tid +++ /dev/null @@ -1,20 +0,0 @@ -title: $:/plugins/tiddlywiki/tiddlyweb/ServerControlPanel -caption: Server -tags: $:/tags/ControlPanel - -<$reveal state="$:/status/IsLoggedIn" type="nomatch" text="yes"> -Log in to ~TiddlyWeb: <$button message="tm-login">Login - -<$reveal state="$:/status/IsLoggedIn" type="match" text="yes"> -Logged in as {{$:/status/UserName}} <$button message="tm-logout">Logout - - ----- - -Host configuration: <$edit-text tiddler="$:/config/tiddlyweb/host" tag="input" default=""/> - -
//for example, `$protocol$//$host$/folder`, where `$protocol$` is replaced by the protocol (typically `http` or `https`), and `$host$` by the host name//
- ----- - -<$button message="tm-server-refresh">Refresh to fetch changes from the server immediately diff --git a/plugins/tiddlywiki/tiddlyweb/config-tiddlers-filter.tid b/plugins/tiddlywiki/tiddlyweb/config-tiddlers-filter.tid new file mode 100644 index 00000000000..04bf24613b3 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/config-tiddlers-filter.tid @@ -0,0 +1,2 @@ +title: $:/config/Server/ExternalFilters/[all[tiddlers]] -[[$:/isEncrypted]] -[prefix[$:/temp/]] -[prefix[$:/status/]] +text: yes diff --git a/plugins/tiddlywiki/tiddlyweb/configOfficialPluginLibrary.tid b/plugins/tiddlywiki/tiddlyweb/configOfficialPluginLibrary.tid new file mode 100644 index 00000000000..9f0e164f414 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/configOfficialPluginLibrary.tid @@ -0,0 +1,3 @@ +title: $:/config/OfficialPluginLibrary + +(This core tiddler is overridden by the tiddlyweb plugin to prevent users from installing official plugins via control panel. Instead they should be installed by editing tiddlywiki.info in the root of the wiki folder) \ No newline at end of file diff --git a/plugins/tiddlywiki/tiddlyweb/css-tiddler.tid b/plugins/tiddlywiki/tiddlyweb/css-tiddler.tid new file mode 100644 index 00000000000..2d7367fa93d --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/css-tiddler.tid @@ -0,0 +1,7 @@ +title: $:/core/templates/css-tiddler + +`` data-tiddler-revision="`<>`" data-tiddler-bag="default" type="text/css">`<$view field="text" format="text" />`` \ No newline at end of file diff --git a/plugins/tiddlywiki/tiddlyweb/html-div-skinny-tiddler.tid b/plugins/tiddlywiki/tiddlyweb/html-div-skinny-tiddler.tid new file mode 100644 index 00000000000..010a603b55c --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/html-div-skinny-tiddler.tid @@ -0,0 +1,9 @@ +title: $:/core/templates/html-div-skinny-tiddler + +`` revision="`<>`" bag="default" _is_skinny=""> +

+
` diff --git a/plugins/tiddlywiki/tiddlyweb/icon-cloud.tid b/plugins/tiddlywiki/tiddlyweb/icon-cloud.tid new file mode 100644 index 00000000000..08c5127afae --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/icon-cloud.tid @@ -0,0 +1,4 @@ +title: $:/plugins/tiddlywiki/tiddlyweb/icon/cloud +tags: $:/tags/Image + + \ No newline at end of file diff --git a/plugins/tiddlywiki/tiddlyweb/javascript-tiddler.tid b/plugins/tiddlywiki/tiddlyweb/javascript-tiddler.tid new file mode 100644 index 00000000000..8478738111b --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/javascript-tiddler.tid @@ -0,0 +1,7 @@ +title: $:/core/templates/javascript-tiddler + +`` data-tiddler-revision="`<>`" data-tiddler-bag="default" type="text/javascript">`<$view field="text" format="text" />`` \ No newline at end of file diff --git a/plugins/tiddlywiki/tiddlyweb/save-offline.tid b/plugins/tiddlywiki/tiddlyweb/save-offline.tid index b2bfdbdd171..76f07fe290d 100644 --- a/plugins/tiddlywiki/tiddlyweb/save-offline.tid +++ b/plugins/tiddlywiki/tiddlyweb/save-offline.tid @@ -2,6 +2,6 @@ title: $:/plugins/tiddlywiki/tiddlyweb/save/offline \import [[$:/core/ui/PageMacros]] [all[shadows+tiddlers]tag[$:/tags/Macro]!has[draft.of]] \define saveTiddlerFilter() -[is[tiddler]] -[[$:/boot/boot.css]] -[[$:/HistoryList]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[[$:/plugins/tiddlywiki/filesystem]] -[[$:/plugins/tiddlywiki/tiddlyweb]] +[sort[title]] $(publishFilter)$ +[is[tiddler]] -[[$:/boot/boot.css]] -[[$:/HistoryList]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[[$:/plugins/tiddlywiki/filesystem]] -[[$:/plugins/tiddlywiki/tiddlyweb]] -[prefix[$:/temp/]] +[sort[title]] $(publishFilter)$ \end {{$:/core/templates/tiddlywiki5.html}} diff --git a/plugins/tiddlywiki/tiddlyweb/save-wiki-button.tid b/plugins/tiddlywiki/tiddlyweb/save-wiki-button.tid new file mode 100644 index 00000000000..b0f688349d0 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/save-wiki-button.tid @@ -0,0 +1,25 @@ +title: $:/core/ui/Buttons/save-wiki +tags: $:/tags/PageControls +caption: {{$:/plugins/tiddlywiki/tiddlyweb/icon/cloud}} Server status +description: Status of synchronisation with server + +\define config-title() +$:/config/PageControlButtons/Visibility/$(listItem)$ +\end +<$button popup=<> tooltip="Status of synchronisation with server" aria-label="Server status" class=<> selectedClass="tc-selected"> + +<$list filter="[match[yes]]"> +{{$:/plugins/tiddlywiki/tiddlyweb/icon/cloud}} + +<$list filter="[match[yes]]"> +<$text text="Server status"/> + + + +<$reveal state=<> type="popup" position="below" animate="yes"> +
+<$list filter="[all[shadows+tiddlers]tag[$:/tags/SyncerDropdown]!has[draft.of]]" variable="listItem"> +<$transclude tiddler=<>/> + +
+ diff --git a/plugins/tiddlywiki/tiddlyweb/styles.tid b/plugins/tiddlywiki/tiddlyweb/styles.tid new file mode 100644 index 00000000000..5d59fa37923 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/styles.tid @@ -0,0 +1,40 @@ +title: $:/plugins/tiddlywiki/tiddlyweb/styles +tags: [[$:/tags/Stylesheet]] + +\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline macrocallblock + +body.tc-dirty span.tc-dirty-indicator svg { + transition: fill 250ms ease-in-out; +} + +body .tc-image-cloud-idle { + fill: <>; + transition: opacity 250ms ease-in-out; + opacity: 1; +} + +body.tc-dirty .tc-image-cloud-idle { + opacity: 0; +} + +body .tc-image-cloud-progress { + transition: opacity 250ms ease-in-out; + transform-origin: 50% 50%; + transform: rotate(359deg); + animation: animation-rotate-slow 2s infinite linear; + fill: <>; + opacity: 0; +} + +body.tc-dirty .tc-image-cloud-progress { + opacity: 1; +} + +@keyframes animation-rotate-slow { + from { + transform: rotate(0deg); + } + to { + transform: scale(359deg); + } +} diff --git a/plugins/tiddlywiki/tiddlyweb/syncer-actions-copy-logs.tid b/plugins/tiddlywiki/tiddlyweb/syncer-actions-copy-logs.tid new file mode 100644 index 00000000000..b141670e687 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/syncer-actions-copy-logs.tid @@ -0,0 +1,6 @@ +title: $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/copy-logs +tags: $:/tags/SyncerDropdown + +<$button message="tm-copy-syncer-logs-to-clipboard" class="tc-btn-invisible"> +{{$:/core/images/copy-clipboard}} Copy syncer logs to clipboard + diff --git a/plugins/tiddlywiki/tiddlyweb/syncer-actions-login-status.tid b/plugins/tiddlywiki/tiddlyweb/syncer-actions-login-status.tid new file mode 100644 index 00000000000..11816f1b47d --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/syncer-actions-login-status.tid @@ -0,0 +1,9 @@ +title: $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/login-status +tags: $:/tags/SyncerDropdown + +<$reveal state="$:/status/IsLoggedIn" type="match" text="yes"> +
+You are logged in<$reveal state="$:/status/UserName" type="nomatch" text="" default=""> as <$text text={{$:/status/UserName}}/><$reveal state="$:/status/IsReadOnly" type="match" text="yes" default="no"> (read-only) +
+
+ diff --git a/plugins/tiddlywiki/tiddlyweb/syncer-actions-login.tid b/plugins/tiddlywiki/tiddlyweb/syncer-actions-login.tid new file mode 100644 index 00000000000..cdd95f5a654 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/syncer-actions-login.tid @@ -0,0 +1,8 @@ +title: $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/login +tags: $:/tags/SyncerDropdown + +<$reveal state="$:/status/IsLoggedIn" type="nomatch" text="yes"> +<$button message="tm-login" class="tc-btn-invisible"> +{{$:/core/images/unlocked-padlock}} Login + + diff --git a/plugins/tiddlywiki/tiddlyweb/syncer-actions-logout.tid b/plugins/tiddlywiki/tiddlyweb/syncer-actions-logout.tid new file mode 100644 index 00000000000..358944d1a12 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/syncer-actions-logout.tid @@ -0,0 +1,8 @@ +title: $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/logout +tags: $:/tags/SyncerDropdown + +<$reveal state="$:/status/IsLoggedIn" type="match" text="yes"> +<$button message="tm-logout" class="tc-btn-invisible"> +{{$:/core/images/cancel-button}} Logout + + diff --git a/plugins/tiddlywiki/tiddlyweb/syncer-actions-refresh.tid b/plugins/tiddlywiki/tiddlyweb/syncer-actions-refresh.tid new file mode 100644 index 00000000000..eeb0ddba2b7 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/syncer-actions-refresh.tid @@ -0,0 +1,9 @@ +title: $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/refresh +tags: $:/tags/SyncerDropdown + +<$reveal state="$:/status/IsLoggedIn" type="match" text="yes"> +<$button tooltip="Get latest changes from the server" aria-label="Refresh from server" class="tc-btn-invisible"> +<$action-sendmessage $message="tm-server-refresh"/> +{{$:/core/images/refresh-button}} <$text text="Get latest changes from the server"/> + + diff --git a/plugins/tiddlywiki/tiddlyweb/syncer-actions-save-snapshot.tid b/plugins/tiddlywiki/tiddlyweb/syncer-actions-save-snapshot.tid new file mode 100644 index 00000000000..23bb4c914cd --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/syncer-actions-save-snapshot.tid @@ -0,0 +1,9 @@ +title: $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/save-snapshot +tags: $:/tags/SyncerDropdown + +<$button class="tc-btn-invisible"> +<$wikify name="site-title" text={{$:/config/SaveWikiButton/Filename}}> +<$action-sendmessage $message="tm-download-file" $param={{$:/config/SaveWikiButton/Template}} filename=<>/> + +{{$:/core/images/download-button}} Save snapshot for offline use + diff --git a/plugins/tiddlywiki/tiddlyweb/tags-syncerdropdown.tid b/plugins/tiddlywiki/tiddlyweb/tags-syncerdropdown.tid new file mode 100644 index 00000000000..07135a75b0f --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb/tags-syncerdropdown.tid @@ -0,0 +1,2 @@ +title: $:/tags/SyncerDropdown +list: $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/login-status $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/login $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/refresh $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/logout $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/save-snapshot $:/plugins/tiddlywiki/tiddlyweb/syncer-actions/copy-logs diff --git a/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js b/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js index 4929093240a..5bc4e502db5 100644 --- a/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js +++ b/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js @@ -27,6 +27,12 @@ function TiddlyWebAdaptor(options) { TiddlyWebAdaptor.prototype.name = "tiddlyweb"; +TiddlyWebAdaptor.prototype.supportsLazyLoading = true; + +TiddlyWebAdaptor.prototype.setLoggerSaveBuffer = function(loggerForSaving) { + this.logger.setSaveBuffer(loggerForSaving); +}; + TiddlyWebAdaptor.prototype.isReady = function() { return this.hasStatus; }; @@ -50,6 +56,11 @@ TiddlyWebAdaptor.prototype.getTiddlerInfo = function(tiddler) { }; }; +TiddlyWebAdaptor.prototype.getTiddlerRevision = function(title) { + var tiddler = this.wiki.getTiddler(title); + return tiddler.fields.revision; +}; + /* Get the current status of the TiddlyWeb connection */ @@ -147,6 +158,9 @@ TiddlyWebAdaptor.prototype.getSkinnyTiddlers = function(callback) { var self = this; $tw.utils.httpRequest({ url: this.host + "recipes/" + this.recipe + "/tiddlers.json", + data: { + filter: "[all[tiddlers]] -[[$:/isEncrypted]] -[prefix[$:/temp/]] -[prefix[$:/status/]]" + }, callback: function(err,data) { // Check for errors if(err) { @@ -220,7 +234,7 @@ TiddlyWebAdaptor.prototype.deleteTiddler = function(title,callback,options) { return callback(null); } // If we don't have a bag it means that the tiddler hasn't been seen by the server, so we don't need to delete it - var bag = options.tiddlerInfo.adaptorInfo.bag; + var bag = options.tiddlerInfo.adaptorInfo && options.tiddlerInfo.adaptorInfo.bag; if(!bag) { return callback(null); } diff --git a/themes/tiddlywiki/vanilla/base.tid b/themes/tiddlywiki/vanilla/base.tid index 91e2e6b61a0..2541938c143 100644 --- a/themes/tiddlywiki/vanilla/base.tid +++ b/themes/tiddlywiki/vanilla/base.tid @@ -1450,6 +1450,10 @@ html body.tc-body.tc-single-tiddler-window { fill: <>; } +.tc-drop-down .tc-drop-down-info { + padding-left: 14px; +} + .tc-drop-down p { padding: 0 14px 0 14px; } @@ -1978,24 +1982,26 @@ html body.tc-body.tc-single-tiddler-window { .tc-alerts { position: fixed; - top: 0; + top: 28px; left: 0; - max-width: 500px; + right: 0; + max-width: 50%; z-index: 20000; } .tc-alert { position: relative; - margin: 28px; - padding: 14px 14px 14px 14px; - border: 2px solid <>; + margin: 14px; + padding: 7px; + border: 1px solid <>; background-color: <>; } .tc-alert-toolbar { position: absolute; - top: 14px; - right: 14px; + top: 7px; + right: 7px; + line-height: 0; } .tc-alert-toolbar svg { @@ -2005,6 +2011,12 @@ html body.tc-body.tc-single-tiddler-window { .tc-alert-subtitle { color: <>; font-weight: bold; + font-size: 0.8em; + margin-bottom: 0.5em; +} + +.tc-alert-body > p { + margin: 0; } .tc-alert-highlight {