Create new static index route with ability to create/update bags and recipes

Also introduces a new /.system/filename route for stylesheets, scripts etc.
This commit is contained in:
Jeremy Ruston 2024-03-20 09:44:52 +00:00
parent 1c64646393
commit 6063256439
23 changed files with 305 additions and 319 deletions

View File

@ -32,7 +32,7 @@
},
"scripts": {
"start": "node ./tiddlywiki.js ./editions/multiwikiserver --listen",
"test": "node ./tiddlywiki.js ./editions/test --verbose --version --build index && node ./tiddlywiki ./editions/multiwikiserver/ --listen debug-level=full --mws-test-server http://127.0.0.1:8080/ --quit",
"test": "node ./tiddlywiki.js ./editions/test --verbose --version --build index && node ./tiddlywiki ./editions/multiwikiserver/ --mws-listen debug-level=full --mws-test-server http://127.0.0.1:8080/ --quit",
"lint:fix": "eslint . --fix",
"lint": "eslint ."
},

View File

@ -1,19 +0,0 @@
title: $:/plugins/multiwikiserver/AdminLayout
tags: $:/tags/Layout
name: MultiWikiServer
description: Admin Layout
icon: $:/favicon.ico
\import [subfilter{$:/core/config/GlobalImportFilter}]
<div class="mws-admin-layout">
<!-- The next DIV is needed for the Jasmine test runner to know that the page has loaded -->
<div class="tc-site-title">TiddlyWiki5</div>
{{MultiWikiServer Administration}}
<div class="mws-admin-layout-controls">
<$button>
<$action-setfield $tiddler="$:/layout" text="$:/core/ui/PageTemplate"/>
Switch to TiddlyWiki default user interface
</$button>
</div>
</div>

View File

@ -1,2 +0,0 @@
title: $:/DefaultTiddlers
text: [[MultiWikiServer Administration]]

View File

@ -1,231 +0,0 @@
title: MultiWikiServer Administration
\procedure createBag(name,description,errorTiddler)
\procedure completion-createBag()
\import [subfilter{$:/core/config/GlobalImportFilter}]
<$action-log
status=<<status>>
statusText=<<statusText>>
error=<<error>>
data=<<data>>
headers=<<headers>>
/>
<%if [<error>match[]] %>
<$action-setfield $tiddler=<<errorTiddler>> text=""/>
<$action-sendmessage $message="tm-server-refresh"/>
<%else%>
<$action-setfield $tiddler=<<errorTiddler>> text=<<data>>/>
<%endif%>
\end completion-createBag
<$action-sendmessage
$message="tm-http-request"
url=`/wiki/${ [<name>encodeuricomponent[]] }$/bags/${ [<name>encodeuricomponent[]] }$`
method="PUT"
body=`{"description":"${ [<description>] }$"}`
oncompletion=<<completion-createBag>>
var-errorTiddler=<<errorTiddler>>
/>
\end createBag
\procedure createBagButton(name)
\whitespace trim
<form class="mws-form">
<div class="mws-form-heading">
<$text text="Create a new bag"/>
</div>
<div class="mws-form-fields">
<div class="mws-form-field">
<label class="mws-form-field-description">
Bag name
</label>
<$edit-text tiddler="$:/temp/NewBagName" tag="input" placeholder="(bag name)" class="mws-form-field-input"/>
</div>
<div class="mws-form-field">
<label class="mws-form-field-description">
Bag description
</label>
<$edit-text tiddler="$:/temp/NewBagDescription" tag="input" placeholder="(description)" class="mws-form-field-input"/>
</div>
</div>
<div class="mws-form-status">
<%if [[$:/temp/NewBagError]get[text]else[]!match[]] %>
<div class="mws-form-error">
<$text text={{$:/temp/NewBagError}}/>
</div>
<%endif%>
</div>
<div class="mws-form-buttons">
<$button class="mws-form-button">
<$transclude
$variable="createBag"
name={{$:/temp/NewBagName}}
description={{$:/temp/NewBagDescription}}
errorTiddler="$:/temp/NewBagError"
/>
Create Bag
</$button>
</div>
</form>
\end createBagButton
\procedure createRecipe(name,bag_names,description,errorTiddler)
\procedure completion-createRecipe()
\import [subfilter{$:/core/config/GlobalImportFilter}]
<%if [<error>match[]] %>
<$action-setfield $tiddler=<<errorTiddler>> text=""/>
<$action-sendmessage $message="tm-server-refresh"/>
<%else%>
<$action-setfield $tiddler=<<errorTiddler>> text=<<data>>/>
<%endif%>
\end completion-createRecipe
\procedure emptyArray() []
\function createRecipeJson()
[<bag_names>enlist-input[]] :reduce[<accumulator>!match[]else<emptyArray>jsonset<index>,<currentTiddler>]
\end createRecipeJson
<$action-sendmessage
$message="tm-http-request"
url=`/wiki/${ [<name>encodeuricomponent[]] }$/recipes/${ [<name>encodeuricomponent[]] }$`
method="PUT"
body=`{"bag_names":${ [<createRecipeJson>] }$,"description":"${ [<description>] }$"}`
oncompletion=<<completion-createRecipe>>
var-errorTiddler=<<errorTiddler>>
/>
\end createRecipe
\procedure createRecipeButton()
\whitespace trim
<form class="mws-form">
<div class="mws-form-heading">
<$text text="Create a new recipe"/>
</div>
<div class="mws-form-fields">
<div class="mws-form-field">
<label class="mws-form-field-description">
Recipe name
</label>
<$edit-text tiddler="$:/temp/NewRecipeName" tag="input" placeholder="(recipe name)" class="mws-form-field-input"/>
</div>
<div class="mws-form-field">
<label class="mws-form-field-description">
Bag names
</label>
<$edit-text tiddler="$:/temp/NewRecipeBagNames" tag="input" placeholder="(space separated list of bags)"/>
</div>
<div class="mws-form-field">
<label class="mws-form-field-description">
Recipe description
</label>
<$edit-text tiddler="$:/temp/NewRecipeDescription" tag="input" placeholder="(description)" class="mws-form-field-input"/>
</div>
</div>
<div class="mws-form-status">
<%if [[$:/temp/NewRecipeError]get[text]else[]!match[]] %>
<div class="mws-form-error">
<$text text={{$:/temp/NewRecipeError}}/>
</div>
<%endif%>
</div>
<div class="mws-form-buttons">
<$button class="mws-form-button">
<$transclude
$variable="createRecipe"
name={{$:/temp/NewRecipeName}}
bag_names={{$:/temp/NewRecipeBagNames}}
description={{$:/temp/NewRecipeDescription}}
errorTiddler="$:/temp/NewRecipeError"
/>
Create Recipe
</$button>
</div>
</form>
\end createRecipeButton
<!-- Expects currentTiddler to be the title of a bag entity state tiddler -->
\procedure bagPill(element-tag:"span",is-topmost:"no")
\whitespace trim
<$genesis $type=<<element-tag>> class={{{ mws-bag-pill [<is-topmost>match[yes]then[mws-bag-pill-topmost]] +[join[ ]] }}}>
<a class="mws-bag-pill-link" href=`/wiki/${ [{!!bag-name}encodeuricomponent[]] }$/bags/${ [{!!bag-name}encodeuricomponent[]] }$` rel="noopener noreferrer" target="_blank">
<$image
source=`/wiki/${ [{!!bag-name}encodeuricomponent[]] }$/bags/${ [{!!bag-name}encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico`
class="mws-favicon-small"
>
<$image
source="$:/plugins/multiwikiserver/images/missing-favicon.png"
class="mws-favicon-small"
/>
</$image>
<span class="mws-bag-pill-label">
<$text text={{!!bag-name}}/>
</span>
</a>
</$genesis>
\end
<!-- Expects currentTiddler to be the title of a recipe entity state tiddler -->
\procedure wikiCard()
\whitespace trim
<a class="mws-wiki-card" href=`/wiki/${ [{!!recipe-name}encodeuricomponent[]] }$` rel="noopener noreferrer" target="_blank">
<div class="mws-wiki-card-image">
<$image
source=`/wiki/${ [{!!recipe-name}encodeuricomponent[]] }$/recipes/${ [{!!recipe-name}encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico`
class="mws-favicon"
>
<$image
source="$:/plugins/multiwikiserver/images/missing-favicon.png"
class="mws-favicon"
/>
</$image>
</div>
<div class="mws-wiki-card-content">
<div class="mws-wiki-card-header">
<$text text={{!!recipe-name}}/>
</div>
<div class="mws-wiki-card-meta">
<%if [list<currentTiddler>] %>
<ol class="mws-horizontal-list">
<$list filter="[list<currentTiddler>]" counter="counter">
<$transclude $variable="bagPill" is-topmost={{{ [<counter-last>match[yes]] }}} element-tag="li"/>
</$list>
</ol>
<%else%>
(no bags defined)
<%endif%>
</div>
<div class="mws-wiki-card-description">
<$text text={{!!text}}/>
</div>
</div>
</a>
\end
<div class="mws-admin-container">
<h1>Wikis</h1>
<p>
These are the wikis available on this server. Click on a wiki to visit it in a new browser tab.
</p>
<ul class="mws-vertical-list">
<$list filter="[prefix[$:/state/MultiWikiServer/recipes/]]">
<li>
<<wikiCard>>
</li>
</$list>
</ul>
<div>
<<createRecipeButton>>
</div>
<div>
Higher numbered bags take priority if a tiddler with the same title is in more than one bag
</div>
<h1>Bags</h1>
<ul class="mws-vertical-list">
<$list filter="[prefix[$:/state/MultiWikiServer/bags/]]">
<li>
<<bagPill>>
<$text text={{!!text}}/>
</li>
</$list>
</ul>
<div>
<<createBagButton>>
</div>
</div>

View File

@ -1,10 +0,0 @@
title: $:/plugins/multiwikiserver/SideBarSegment
tags: $:/tags/SideBarSegment
list-before: $:/core/ui/SideBarSegments/page-controls
<div class="mws-admin-sidebar">
<$button>
<$action-setfield $tiddler="$:/layout" text="$:/plugins/multiwikiserver/AdminLayout"/>
Switch back to ~MultiWikiServer administration user interface
</$button>
</div>

View File

@ -1,2 +0,0 @@
title: $:/favicon.ico
type: image/png

View File

@ -1,2 +0,0 @@
title: $:/layout
text: $:/plugins/multiwikiserver/AdminLayout

View File

@ -1,2 +0,0 @@
title: $:/plugins/multiwikiserver/images/missing-favicon.png
type: image/png

View File

@ -33,7 +33,8 @@ Command.prototype.execute = function() {
}
// Set up server
this.server = $tw.mws.serverManager.createServer({
wiki: $tw.wiki
wiki: $tw.wiki,
variables: self.params
});
this.server.listen(null,null,null,{
callback: function() {

View File

@ -74,6 +74,7 @@ TestRunner.prototype.runTests = function(callback) {
};
TestRunner.prototype.runTest = function(testSpec,callback) {
const self = this;
console.log(`Running Server Test: ${testSpec.description}`)
if(testSpec.method === "GET" || testSpec.method === "POST") {
const request = this.httpLibrary.request({
@ -84,8 +85,8 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
method: testSpec.method,
headers: testSpec.headers
}, function(response) {
if (response.statusCode < 200 || response.statusCode >= 300) {
return callback(`Request failed to ${response.url} with status code ${response.statusCode} and ${JSON.stringify(response.headers)}`);
if (response.statusCode < 200 || response.statusCode >= 400) {
return callback(`Request failed to ${self.urlServerParsed.toString()} with status code ${response.statusCode} and ${JSON.stringify(response.headers)}`);
}
response.setEncoding("utf8");
let buffer = "";
@ -94,7 +95,7 @@ TestRunner.prototype.runTest = function(testSpec,callback) {
});
response.on("end", () => {
const jsonData = $tw.utils.parseJSONSafe(buffer,function() {return undefined;});
const testResult = testSpec.expectedResult(jsonData,buffer);
const testResult = testSpec.expectedResult(jsonData,buffer,response.headers);
callback(testResult ? null : "Test failed");
});
});
@ -135,6 +136,20 @@ const testSpecs = [
expectedResult: (jsonData,data) => {
return jsonData["imported-tiddlers"] && $tw.utils.isArray(jsonData["imported-tiddlers"]) && jsonData["imported-tiddlers"][0] === "One White Pixel";
}
},
{
description: "Create a recipe",
method: "POST",
path: "/recipes",
headers: {
"Accept": '*/*',
"Content-Type": 'application/x-www-form-urlencoded',
"User-Agent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
},
data: "recipe_name=Elephants3214234&bag_names=one%20two%20three&description=A%20bag%20of%20elephants",
expectedResult: (jsonData,data,headers) => {
return headers.location === "/";
}
}
];

View File

@ -316,7 +316,8 @@ Server.prototype.findMatchingRoute = function(request,state) {
} else {
match = potentialRoute.path.exec(pathname);
}
if(match && request.method === potentialRoute.method) {
// Allow POST as a synonym for PUT because HTML doesn't allow PUT forms
if(match && (request.method === potentialRoute.method || (request.method === "POST" && potentialRoute.method === "PUT"))) {
state.params = [];
for(var p=1; p<match.length; p++) {
state.params.push(match[p]);
@ -346,6 +347,7 @@ Server.prototype.isAuthorized = function(authorizationType,username) {
Server.prototype.requestHandler = function(request,response,options) {
options = options || {};
const queryString = require("querystring");
// Compose the state object
var self = this;
var state = {};
@ -399,7 +401,7 @@ Server.prototype.requestHandler = function(request,response,options) {
if(route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") {
// Let the route handle the request stream itself
route.handler(request,response,state);
} else if(route.bodyFormat === "string" || !route.bodyFormat) {
} else if(route.bodyFormat === "string" || route.bodyFormat === "www-form-urlencoded" || !route.bodyFormat) {
// Set the encoding for the incoming request
request.setEncoding("utf8");
var data = "";
@ -407,6 +409,9 @@ Server.prototype.requestHandler = function(request,response,options) {
data += chunk.toString();
});
request.on("end",function() {
if(route.bodyFormat === "www-form-urlencoded") {
data = queryString.parse(data);
}
state.data = data;
route.handler(request,response,state);
});

View File

@ -0,0 +1,52 @@
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/get-system.js
type: application/javascript
module-type: mws-route
Retrieves a system file. System files are stored in configuration tiddlers with the following fields:
* title: "$:/plugins/tiddlywiki/multiwikiserver/system-files/" suffixed with the name of the file
* tags: tagged $:/tags/MWS/SystemFile or $:/tags/MWS/SystemFileWikified
* system-file-type: optionally specify the MIME type that should be returned for the file
GET /.system/:filename
\*/
(function() {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "GET";
exports.path = /^\/\.system\/(.+)$/;
const SYSTEM_FILE_TITLE_PREFIX = "$:/plugins/tiddlywiki/multiwikiserver/system-files/";
exports.handler = function(request,response,state) {
// Get the parameters
const filename = $tw.utils.decodeURIComponentSafe(state.params[0]),
title = SYSTEM_FILE_TITLE_PREFIX + filename,
tiddler = $tw.wiki.getTiddler(title),
isSystemFile = tiddler && tiddler.hasTag("$:/tags/MWS/SystemFile"),
isSystemFileWikified = tiddler && tiddler.hasTag("$:/tags/MWS/SystemFileWikified");
if(tiddler && (isSystemFile || isSystemFileWikified)) {
let text = tiddler.fields.text || "";
const type = tiddler.fields["system-file-type"] || tiddler.fields.type || "text/plain",
encoding = ($tw.config.contentTypeInfo[type] ||{encoding: "utf8"}).encoding;
if(isSystemFileWikified) {
text = $tw.wiki.renderTiddler("text/plain",title);
}
response.writeHead(200, "OK",{
"Content-Type": type
});
response.write(text,encoding);
response.end();
} else {
response.writeHead(404);
response.end();
}
};
}());

View File

@ -0,0 +1,50 @@
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-bag.js
type: application/javascript
module-type: mws-route
POST /bags
Parameters:
bag_name
description
\*/
(function() {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "POST";
exports.path = /^\/bags$/;
exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.handler = function(request,response,state) {
if(state.data.bag_name) {
const result = $tw.mws.store.createBag(state.data.bag_name,state.data.description);
if(!result) {
state.sendResponse(302,{
"Content-Type": "text/plain",
"Location": "/"
});
} else {
state.sendResponse(400,{
"Content-Type": "text/plain"
},
result.message,
"utf8");
}
} else {
state.sendResponse(400,{
"Content-Type": "text/plain"
});
}
};
}());

View File

@ -0,0 +1,51 @@
/*\
title: $:/plugins/tiddlywiki/multiwikiserver/routes/handlers/post-recipe.js
type: application/javascript
module-type: mws-route
POST /recipes
Parameters:
recipe_name
description
bag_names: space separated list of bags
\*/
(function() {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.method = "POST";
exports.path = /^\/recipes$/;
exports.bodyFormat = "www-form-urlencoded";
exports.csrfDisable = true;
exports.handler = function(request,response,state) {
if(state.data.recipe_name && state.data.bag_names) {
const result = $tw.mws.store.createRecipe(state.data.recipe_name,$tw.utils.parseStringArray(state.data.bag_names),state.data.description);
if(!result) {
state.sendResponse(302,{
"Content-Type": "text/plain",
"Location": "/"
});
} else {
state.sendResponse(400,{
"Content-Type": "text/plain"
},
result.message,
"utf8");
}
} else {
state.sendResponse(400,{
"Content-Type": "text/plain"
});
}
};
}());

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,3 @@
title: $:/plugins/tiddlywiki/multiwikiserver/system-files/missing-favicon.png
tags: $:/tags/MWS/SystemFile
type: image/png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,3 @@
title: $:/plugins/tiddlywiki/multiwikiserver/system-files/motovun-jack.jpg
tags: $:/tags/MWS/SystemFile
type: image/jpeg

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,3 @@
title: $:/plugins/tiddlywiki/multiwikiserver/system-files/mws-icon.png
tags: $:/tags/MWS/SystemFile
type: image/png

View File

@ -1,23 +1,19 @@
title: $:/plugins/multiwikiserver/Styles
tags: $:/tags/Stylesheet
title: $:/plugins/tiddlywiki/multiwikiserver/system-files/styles.css
tags: $:/tags/MWS/SystemFileWikified
system-file-type: text/css
\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline macrocallblock
/*
Styles specific to the full screen layout
*/
/* Import TiddlyWiki theme styles */
.mws-admin-layout {
{{$:/core/ui/PageStylesheet}}
/* MWS Styles */
body {
padding: 1rem;
}
.mws-form-error {
border: 2px solid red;
padding: 1em;
margin: 1em;
}
.mws-wiki-card {
display: flex;
margin: 1em 0;
@ -62,6 +58,7 @@ Styles specific to the full screen layout
.mws-vertical-list {
list-style: none;
padding: 0;
line-height: 1.5;
}
.mws-horizontal-list {

View File

@ -1,62 +1,133 @@
title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-index
\procedure bagPill(element-tag:"span",is-topmost:"no")
\whitespace trim
<$genesis $type=<<element-tag>> class={{{ mws-bag-pill [<is-topmost>match[yes]then[mws-bag-pill-topmost]] +[join[ ]] }}}>
<a class="mws-bag-pill-link" href=`/wiki/${ [<bag-name>encodeuricomponent[]] }$/bags/${ [<bag-name>encodeuricomponent[]] }$` rel="noopener noreferrer" target="_blank">
<img
src=`/wiki/${ [<bag-name>encodeuricomponent[]] }$/bags/${ [<bag-name>encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico`
class="mws-favicon-small"
/>
<span class="mws-bag-pill-label">
<$text text=<<bag-name>>/>
</span>
</a>
</$genesis>
\end
! Wikis Available Here
<ul>
<ul class="mws-vertical-list">
<$list filter="[<recipe-list>jsonindexes[]] :sort[<currentTiddler>jsonget[recipe_name]]" variable="recipe-index">
<li>
<$let
recipe-info={{{ [<recipe-list>jsonextract<recipe-index>] }}}
recipe-name={{{ [<recipe-info>jsonget[recipe_name]] }}}
>
<a
href=`/wiki/${ [<recipe-name>encodeuricomponent[]] }$`
rel="noopener noreferrer"
target="_blank"
<div
class="mws-wiki-card"
>
<strong>
<$text text={{{ [<recipe-info>jsonget[recipe_name]] }}}/>
</strong>
: <$text text={{{ [<recipe-info>jsonget[description]] }}}/>
</a>
<ul>
<$list filter="[<recipe-info>jsonget[bag_names]]" variable="bag-name">
<li>
<div class="mws-wiki-card-image">
<img
src=`/wiki/${ [<recipe-name>encodeuricomponent[]] }$/recipes/${ [<recipe-name>encodeuricomponent[]] }$/tiddlers/%24%3A%2Ffavicon.ico`
class="mws-favicon"
/>
</div>
<div class="mws-wiki-card-content">
<div class="mws-wiki-card-header">
<a
href=`/wiki/${ [<bag-name>encodeuricomponent[]] }$/bags/${ [<bag-name>encodeuricomponent[]] }$`
href=`/wiki/${ [<recipe-name>encodeuricomponent[]] }$`
rel="noopener noreferrer"
target="_blank"
>
<$text text=<<bag-name>>/>
<$text text={{{ [<recipe-info>jsonget[recipe_name]] }}}/>
</a>
</li>
</$list>
</ul>
</div>
<div class="mws-wiki-card-meta">
<%if true %>
<ol class="mws-horizontal-list">
<$list filter="[<recipe-info>jsonget[bag_names]]" variable="bag-name" counter="counter">
<$transclude $variable="bagPill" is-topmost={{{ [<counter-last>match[yes]] }}} element-tag="li"/>
</$list>
</ol>
<%else%>
(no bags defined)
<%endif%>
</div>
<div class="mws-wiki-card-description">
<$text text={{{ [<recipe-info>jsonget[description]] }}}/>
</div>
</div>
</div>
</$let>
</li>
</$list>
</ul>
<form action="/recipes" method="post" class="mws-form">
<div class="mws-form-heading">
Create a new recipe or modify and existing one
</div>
<div class="mws-form-fields">
<div class="mws-form-field">
<label class="mws-form-field-description">
Recipe name
</label>
<input name="recipe_name" type="text"/>
</div>
<div class="mws-form-field">
<label class="mws-form-field-description">
Recipe description
</label>
<input name="description" type="text"/>
</div>
<div class="mws-form-field">
<label class="mws-form-field-description">
Bags in recipe (space separated)
</label>
<input name="bag_names" type="text"/>
</div>
</div>
<div class="mws-form-buttons">
<input type="submit" value="Create or Update Recipe" formmethod="post"/>
</div>
</form>
! Bags
<ul>
<$list filter="[<bag-list>jsonindexes[]] :sort[<currentTiddler>jsonget[bag_name]]" variable="bag-index">
<li>
<ul class="mws-vertical-list">
<$list filter="[<bag-list>jsonindexes[]] :sort[<currentTiddler>jsonget[bag_name]]" variable="bag-index" counter="counter">
<li class="mws-wiki-card">
<$let
bag-info={{{ [<bag-list>jsonextract<bag-index>] }}}
bag-name={{{ [<bag-info>jsonget[bag_name]] }}}
>
<a
href=`/wiki/${ [<bag-name>encodeuricomponent[]] }$/bags/${ [<bag-name>encodeuricomponent[]] }$`
rel="noopener noreferrer"
target="_blank"
>
<strong>
<$text text={{{ [<bag-info>jsonget[bag_name]] }}}/>
</strong>
: <$text text={{{ [<bag-info>jsonget[description]] }}}/>
</a>
<$transclude $variable="bagPill"/>
<$text text={{{ [<bag-info>jsonget[description]] }}}/>
</$let>
</li>
</$list>
</ul>
<form action="/bags" method="post" class="mws-form">
<div class="mws-form-heading">
Create a new bag or modify and existing one
</div>
<div class="mws-form-fields">
<div class="mws-form-field">
<label class="mws-form-field-description">
Bag name
</label>
<input name="bag_name" type="text"/>
</div>
<div class="mws-form-field">
<label class="mws-form-field-description">
Bag description
</label>
<input name="description" type="text"/>
</div>
</div>
<div class="mws-form-buttons">
<input type="submit" value="Create or Update Bag" formmethod="post"/>
</div>
</form>

View File

@ -9,11 +9,14 @@ page-content: title of tiddler containing the main page content
<!doctype html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<link rel="stylesheet" href="/.system/styles.css">
</head>
<body>
<div class="pageContainer">
`
<$view tiddler=<<page-content>> field="text" format="htmlwikified" />
`
</div>
</body>
</html>
`