1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-01-22 06:56:52 +00:00

Add support for bag descriptions, validate bags and recipes, and complete the UI for creating bags and recipes

Styling to come
This commit is contained in:
Jeremy Ruston 2024-01-24 22:24:24 +00:00
parent 0b9749f3a4
commit b0a67300cc
6 changed files with 254 additions and 109 deletions

View File

@ -1,49 +1,111 @@
title: MultiWikiServer Administration title: MultiWikiServer Administration
\procedure createBag(name) \procedure createBag(name,description)
\procedure completion-createBag() \procedure completion-createBag()
\import [subfilter{$:/core/config/GlobalImportFilter}] \import [subfilter{$:/core/config/GlobalImportFilter}]
<$action-log msg="In completion-createBag"/> <$action-log msg="In completion-createBag"/>
<$action-log/> <$action-log/>
\end completion-createBag \end completion-createBag
<$action-sendmessage <$action-sendmessage
$message="tm-http-request" $message="tm-http-request"
url=`/wiki/$(name)$/bags/$(name)$` url=`/wiki/$(name)$/bags/$(name)$`
method="PUT" method="PUT"
body=`{"description":"${ [<description>encodeuricomponent[]] }$"}`
oncompletion=<<completion-createBag>> oncompletion=<<completion-createBag>>
/> />
\end createBag \end createBag
\procedure createBagButton(name) \procedure createBagButton(name)
<$button class=""> \whitespace trim
<$transclude $variable="createBag" name={{$:/state/NewBagName}}/> <form class="mws-form">
{{$:/core/images/new-button}} <div class="mws-form-heading">
</$button><span class="tc-btn-text"><$text text="Create a new bag:"/></span><$edit-text tiddler="$:/state/NewBagName" tag="input"/> <$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="$:/state/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="$:/state/NewBagDescription" tag="input" placeholder="(description)" class="mws-form-field-input"/>
</div>
</div>
<div class="mws-form-buttons">
<$button class="mws-form-button">
<$transclude
$variable="createBag"
name={{$:/state/NewBagName}}
description={{$:/state/NewBagDescription}}
/>
Create Bag
</$button>
</div>
</form>
\end createBagButton \end createBagButton
\procedure createRecipe(name) \procedure createRecipe(name,bag_names,description)
\procedure completion-createRecipe() \procedure completion-createRecipe()
\import [subfilter{$:/core/config/GlobalImportFilter}] \import [subfilter{$:/core/config/GlobalImportFilter}]
<$action-log msg="In completion-createRecipe"/> <$action-log msg="In completion-createRecipe"/>
<$action-log/> <$action-log/>
\end completion-createRecipe \end completion-createRecipe
\procedure emptyArray() []
\function createRecipeJson()
[<bag_names>enlist-input[]] :reduce[<accumulator>!match[]else<emptyArray>jsonset<index>,<currentTiddler>]
\end createRecipeJson
<$action-log message="Sending" body=<<createRecipeJson>>/>
<$action-sendmessage <$action-sendmessage
$message="tm-http-request" $message="tm-http-request"
url=`/wiki/$(name)$/recipes/$(name)$` url=`/wiki/$(name)$/recipes/$(name)$`
method="PUT" method="PUT"
body=`{"bag_names":${ [<createRecipeJson>] }$,"description":"${ [<description>encodeuricomponent[]] }$"}`
oncompletion=<<completion-createRecipe>> oncompletion=<<completion-createRecipe>>
/> />
\end createRecipe \end createRecipe
\procedure createRecipeButton(name) \procedure createRecipeButton()
<$button class=""> \whitespace trim
<$transclude $variable="createRecipe" name={{$:/state/NewRecipeName}}/> <form class="mws-form">
{{$:/core/images/new-button}} <div class="mws-form-heading">
</$button><span class="tc-btn-text"><$text text="Create a new recipe:"/></span><$edit-text tiddler="$:/state/NewRecipeName" tag="input"/> <$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="$:/state/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="$:/state/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="$:/state/NewRecipeDescription" tag="input" placeholder="(description)" class="mws-form-field-input"/>
</div>
</div>
<div class="mws-form-buttons">
<$button class="mws-form-button">
<$transclude
$variable="createRecipe"
name={{$:/state/NewRecipeName}}
bag_names={{$:/state/NewRecipeBagNames}}
description={{$:/state/NewRecipeDescription}}
/>
Create Recipe
</$button>
</div>
</form>
\end createRecipeButton \end createRecipeButton
<!-- Expects currentTiddler to be the title of a bag entity state tiddler --> <!-- Expects currentTiddler to be the title of a bag entity state tiddler -->
@ -127,6 +189,7 @@ title: MultiWikiServer Administration
<$list filter="[prefix[$:/state/MultiWikiServer/bags/]]"> <$list filter="[prefix[$:/state/MultiWikiServer/bags/]]">
<li> <li>
<<bagPill>> <<bagPill>>
<$text text={{!!text}}/>
</li> </li>
</$list> </$list>
</ul> </ul>

View File

@ -42,16 +42,16 @@ exports.startup = function() {
databasePath: databasePath databasePath: databasePath
}); });
// Create docs bag and recipe // Create docs bag and recipe
$tw.sqlTiddlerStore.createBag("docs"); $tw.sqlTiddlerStore.createBag("docs","TiddlyWiki Documentation from https://tiddlywiki.com/");
$tw.sqlTiddlerStore.createRecipe("docs",["docs"],"TiddlyWiki Documentation from https://tiddlywiki.com/"); $tw.sqlTiddlerStore.createRecipe("docs",["docs"],"TiddlyWiki Documentation from https://tiddlywiki.com/");
$tw.sqlTiddlerStore.saveTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,"tw5.com/tiddlers"),"docs"); $tw.sqlTiddlerStore.saveTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,"tw5.com/tiddlers"),"docs");
$tw.sqlTiddlerStore.createBag("dev-docs"); $tw.sqlTiddlerStore.createBag("dev-docs","TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev/");
$tw.sqlTiddlerStore.createRecipe("dev-docs",["dev-docs"],"TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev/"); $tw.sqlTiddlerStore.createRecipe("dev-docs",["dev-docs"],"TiddlyWiki Developer Documentation from https://tiddlywiki.com/dev/");
$tw.sqlTiddlerStore.saveTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,"dev/tiddlers"),"dev-docs"); $tw.sqlTiddlerStore.saveTiddlersFromPath(path.resolve($tw.boot.corePath,$tw.config.editionsPath,"dev/tiddlers"),"dev-docs");
// Create bags and recipes // Create bags and recipes
$tw.sqlTiddlerStore.createBag("bag-alpha"); $tw.sqlTiddlerStore.createBag("bag-alpha","A test bag");
$tw.sqlTiddlerStore.createBag("bag-beta"); $tw.sqlTiddlerStore.createBag("bag-beta","Another test bag");
$tw.sqlTiddlerStore.createBag("bag-gamma"); $tw.sqlTiddlerStore.createBag("bag-gamma","A further test bag");
$tw.sqlTiddlerStore.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"First wiki"); $tw.sqlTiddlerStore.createRecipe("recipe-rho",["bag-alpha","bag-beta"],"First wiki");
$tw.sqlTiddlerStore.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"],"Second Wiki"); $tw.sqlTiddlerStore.createRecipe("recipe-sigma",["bag-alpha","bag-gamma"],"Second Wiki");
$tw.sqlTiddlerStore.createRecipe("recipe-tau",["bag-alpha"],"Third Wiki"); $tw.sqlTiddlerStore.createRecipe("recipe-tau",["bag-alpha"],"Third Wiki");

View File

@ -21,12 +21,21 @@ exports.path = /^\/wiki\/([^\/]+)\/bags\/([^\/]+)$/;
exports.handler = function(request,response,state) { exports.handler = function(request,response,state) {
// Get the parameters // Get the parameters
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
bag_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]); bag_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]),
if(bag_name === bag_name_2) { data = $tw.utils.parseJSONSafe(state.data);
$tw.sqlTiddlerStore.createBag(bag_name); if(bag_name === bag_name_2 && data) {
const result = $tw.sqlTiddlerStore.createBag(bag_name,data.description);
if(!result) {
state.sendResponse(204,{ state.sendResponse(204,{
"Content-Type": "text/plain" "Content-Type": "text/plain"
}); });
} else {
state.sendResponse(400,{
"Content-Type": "text/plain"
},
result.message,
"utf8");
}
} else { } else {
response.writeHead(404); response.writeHead(404);
response.end(); response.end();

View File

@ -21,12 +21,22 @@ exports.path = /^\/wiki\/([^\/]+)\/recipes\/([^\/]+)$/;
exports.handler = function(request,response,state) { exports.handler = function(request,response,state) {
// Get the parameters // Get the parameters
var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]); recipe_name_2 = $tw.utils.decodeURIComponentSafe(state.params[1]),
if(recipe_name === recipe_name_2) { data = $tw.utils.parseJSONSafe(state.data);
$tw.sqlTiddlerStore.createRecipe(recipe_name); if(recipe_name === recipe_name_2 && data) {
const result = $tw.sqlTiddlerStore.createRecipe(recipe_name,data.bag_names,data.description);
console.log(`create recipe route handler for ${recipe_name} with ${JSON.stringify(data)} got result ${JSON.stringify(result)}`)
if(!result) {
state.sendResponse(204,{ state.sendResponse(204,{
"Content-Type": "text/plain" "Content-Type": "text/plain"
}); });
} else {
state.sendResponse(400,{
"Content-Type": "text/plain"
},
result.message,
"utf8");
}
} else { } else {
response.writeHead(404); response.writeHead(404);
response.end(); response.end();

View File

@ -6,6 +6,7 @@ module-type: library
Low level SQL functions to store and retrieve tiddlers in a SQLite database. Low level SQL functions to store and retrieve tiddlers in a SQLite database.
This class is intended to encapsulate all the SQL queries used to access the database. This class is intended to encapsulate all the SQL queries used to access the database.
Validation is for the most part left to the caller
\*/ \*/
@ -58,7 +59,8 @@ SqlTiddlerDatabase.prototype.createTables = function() {
CREATE TABLE IF NOT EXISTS bags ( CREATE TABLE IF NOT EXISTS bags (
bag_id INTEGER PRIMARY KEY AUTOINCREMENT, bag_id INTEGER PRIMARY KEY AUTOINCREMENT,
bag_name TEXT UNIQUE, bag_name TEXT UNIQUE,
accesscontrol TEXT accesscontrol TEXT,
description TEXT
) )
`,` `,`
-- Recipes have names... -- Recipes have names...
@ -115,28 +117,30 @@ SqlTiddlerDatabase.prototype.logTables = function() {
SqlTiddlerDatabase.prototype.listBags = function() { SqlTiddlerDatabase.prototype.listBags = function() {
const rows = this.runStatementGetAll(` const rows = this.runStatementGetAll(`
SELECT bag_name, accesscontrol SELECT bag_name, accesscontrol, description
FROM bags FROM bags
ORDER BY bag_name ORDER BY bag_name
`); `);
return rows; return rows;
}; };
SqlTiddlerDatabase.prototype.createBag = function(bagname) { SqlTiddlerDatabase.prototype.createBag = function(bagname,description) {
// Run the queries // Run the queries
this.runStatement(` this.runStatement(`
INSERT OR IGNORE INTO bags (bag_name, accesscontrol) INSERT OR IGNORE INTO bags (bag_name, accesscontrol, description)
VALUES ($bag_name, '') VALUES ($bag_name, '', '')
`,{ `,{
bag_name: bagname bag_name: bagname
}); });
this.runStatement(` this.runStatement(`
UPDATE bags UPDATE bags
SET accesscontrol = $accesscontrol SET accesscontrol = $accesscontrol,
description = $description
WHERE bag_name = $bag_name WHERE bag_name = $bag_name
`,{ `,{
bag_name: bagname, bag_name: bagname,
accesscontrol: "[some access control stuff]" accesscontrol: "[some access control stuff]",
description: description
}); });
}; };

View File

@ -7,6 +7,7 @@ Higher level functions to perform basic tiddler operations with a sqlite3 databa
This class is largely a wrapper for the sql-tiddler-database.js class, adding the following functionality: This class is largely a wrapper for the sql-tiddler-database.js class, adding the following functionality:
* Validating requests (eg bag and recipe name constraints)
* Synchronising bag and recipe names to the admin wiki * Synchronising bag and recipe names to the admin wiki
* Handling _canonical_uri tiddlers * Handling _canonical_uri tiddlers
@ -34,6 +35,43 @@ function SqlTiddlerStore(options) {
this.updateAdminWiki(); this.updateAdminWiki();
} }
/*
Returns null if a bag/recipe name is valid, or a string error message if not
*/
SqlTiddlerStore.prototype.validateItemName = function(name) {
if(typeof name !== "string") {
return "Not a valid string";
}
if(name.length > 256) {
return "Too long";
}
if(!(/^[^\s\u00A0\x00-\x1F\x7F~`!@#$%^&*()+={}\[\];:\'\"<>.,\/\\\?]+$/g.test(name))) {
return "Invalid character(s)";
}
return null;
};
/*
Returns null if the argument is an array of valid bag/recipe names, or a string error message if not
*/
SqlTiddlerStore.prototype.validateItemNames = function(names) {
if(!$tw.utils.isArray(names)) {
return "Not a valid array";
}
var errors = [];
for(const name of names) {
const result = this.validateItemName(name);
if(result) {
errors.push(result);
}
}
if(errors.length === 0) {
return null;
} else {
return errors.join("\n");
}
};
SqlTiddlerStore.prototype.close = function() { SqlTiddlerStore.prototype.close = function() {
this.sqlTiddlerDatabase.close(); this.sqlTiddlerDatabase.close();
this.sqlTiddlerDatabase = undefined; this.sqlTiddlerDatabase = undefined;
@ -49,7 +87,7 @@ SqlTiddlerStore.prototype.updateAdminWiki = function() {
this.saveEntityStateTiddler({ this.saveEntityStateTiddler({
title: "bags/" + bagInfo.bag_name, title: "bags/" + bagInfo.bag_name,
"bag-name": bagInfo.bag_name, "bag-name": bagInfo.bag_name,
text: "" text: bagInfo.description
}); });
} }
// Update recipes // Update recipes
@ -112,22 +150,42 @@ SqlTiddlerStore.prototype.listBags = function() {
return this.sqlTiddlerDatabase.listBags(); return this.sqlTiddlerDatabase.listBags();
}; };
SqlTiddlerStore.prototype.createBag = function(bagname) { SqlTiddlerStore.prototype.createBag = function(bagname,description) {
this.sqlTiddlerDatabase.createBag(bagname); console.log(`create bag method for ${bagname} with ${description}`)
console.log(`validation results are ${this.validateItemName(bagname)}`)
const validationBagName = this.validateItemName(bagname);
if(validationBagName) {
return {message: validationBagName};
}
this.sqlTiddlerDatabase.createBag(bagname,description);
this.saveEntityStateTiddler({ this.saveEntityStateTiddler({
title: "bags/" + bagname, title: "bags/" + bagname,
"bag-name": bagname, "bag-name": bagname,
text: "" text: description
}); });
return null;
}; };
SqlTiddlerStore.prototype.listRecipes = function() { SqlTiddlerStore.prototype.listRecipes = function() {
return this.sqlTiddlerDatabase.listRecipes(); return this.sqlTiddlerDatabase.listRecipes();
}; };
/*
Returns null on success, or {message:} on error
*/
SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames,description) { SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames,description) {
console.log(`create recipe method for ${recipename} with ${JSON.stringify(bagnames)}`)
console.log(`validation results are ${this.validateItemName(recipename)} and ${this.validateItemNames(bagnames)}`)
bagnames = bagnames || []; bagnames = bagnames || [];
description = description || ""; description = description || "";
const validationRecipeName = this.validateItemName(recipename);
if(validationRecipeName) {
return {message: validationRecipeName};
}
const validationBagNames = this.validateItemNames(bagnames);
if(validationBagNames) {
return {message: validationBagNames};
}
this.sqlTiddlerDatabase.createRecipe(recipename,bagnames,description); this.sqlTiddlerDatabase.createRecipe(recipename,bagnames,description);
this.saveEntityStateTiddler({ this.saveEntityStateTiddler({
title: "recipes/" + recipename, title: "recipes/" + recipename,
@ -137,6 +195,7 @@ SqlTiddlerStore.prototype.createRecipe = function(recipename,bagnames,descriptio
return this.entityStateTiddlerPrefix + "bags/" + bag_name; return this.entityStateTiddlerPrefix + "bags/" + bag_name;
})) }))
}); });
return null;
}; };
/* /*