1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2025-01-04 14:30:28 +00:00

Add filtering via tags, text, and title in get-bag template

This commit is contained in:
Thomas E Tuoti 2024-12-21 11:06:44 -07:00
parent bc6e58d0af
commit 6f3e70c5d6
4 changed files with 287 additions and 53 deletions

View File

@ -22,21 +22,40 @@ exports.useACL = true;
exports.entityName = "bag"
exports.handler = function (request, response, state) {
// Redirect if there is no trailing slash. We do this so that the relative URL specified in the upload form works correctly
// Redirect if there is no trailing slash
if (state.params[1] !== "/") {
state.redirect(301, state.urlInfo.path + "/");
return;
}
// Get the parameters
// Get the parameters and bag list
var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]),
bagTiddlers = bag_name && $tw.mws.store.getBagTiddlers(bag_name);
filterText = state.queryParameters ? state.queryParameters.filter || "" : "",
filterField = state.queryParameters ? state.queryParameters.field || "title" : "title",
bagTiddlers = bag_name && (filterText ?
$tw.mws.store.sqlTiddlerDatabase.getFilteredBagTiddlers(bag_name, filterText, filterField) :
$tw.mws.store.getBagTiddlers(bag_name)),
bagList = $tw.mws.store.listBags();
if (bag_name && bagTiddlers) {
// If application/json is requested then this is an API request, and gets the response in JSON
// Handle JSON API request
if (request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) {
state.sendResponse(200, { "Content-Type": "application/json" }, JSON.stringify(bagTiddlers), "utf8");
} else {
if (!response.headersSent) {
// This is not a JSON API request, we should return the raw tiddler content
// Filter bags by user's read access from ACL
var allowedBags = bagList.filter(bag =>
bag.bag_name.startsWith("$:/") ||
state.server.sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, bag.bag_name, 'READ') ||
state.allowAnon && state.allowAnonReads
);
// Filter out system tiddlers unless explicitly shown
if (!state.queryParameters.show_system || state.queryParameters.show_system === "off") {
allowedBags = allowedBags.filter(bag => !bag.bag_name.startsWith("$:/"));
}
response.writeHead(200, "OK", {
"Content-Type": "text/html"
});
@ -45,7 +64,10 @@ exports.handler = function (request, response, state) {
"page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-bag",
"bag-name": bag_name,
"bag-titles": JSON.stringify(bagTiddlers.map(bagTiddler => bagTiddler.title)),
"bag-tiddlers": JSON.stringify(bagTiddlers)
"bag-tiddlers": JSON.stringify(bagTiddlers),
"bag-list": JSON.stringify(allowedBags),
"current-filter": filterText,
"current-field": filterField
}
});
response.write(html);
@ -60,4 +82,4 @@ exports.handler = function (request, response, state) {
}
};
}());
}());

View File

@ -41,6 +41,49 @@ SqlTiddlerDatabase.prototype.close = function() {
this.engine.close();
};
SqlTiddlerDatabase.prototype.getFilteredBagTiddlers = function(bag_name, filterText, filterField) {
let query = `
SELECT DISTINCT t.title, t.tiddler_id
FROM tiddlers t
LEFT JOIN fields f ON t.tiddler_id = f.tiddler_id
WHERE t.bag_id IN (
SELECT bag_id
FROM bags
WHERE bag_name = $bag_name
)
AND t.is_deleted = FALSE
`;
const params = {
$bag_name: bag_name,
$filterText: '%' + filterText + '%'
};
// Add field-specific filtering
if (filterField === "tag") {
query += ` AND EXISTS (
SELECT 1 FROM fields
WHERE tiddler_id = t.tiddler_id
AND field_name = 'tags'
AND field_value LIKE $filterText
)`;
} else if (filterField === "text") {
query += ` AND EXISTS (
SELECT 1 FROM fields
WHERE tiddler_id = t.tiddler_id
AND field_name = 'text'
AND field_value LIKE $filterText
)`;
} else {
// Default to title search
query += ` AND t.title LIKE $filterText`;
}
query += ` ORDER BY t.title ASC`;
return this.engine.runStatementGetAll(query, params);
};
SqlTiddlerDatabase.prototype.transaction = function(fn) {
return this.engine.transaction(fn);

View File

@ -54,6 +54,10 @@ SqlTiddlerStore.prototype.removeEventListener = function(type,listener) {
}
};
SqlTiddlerStore.prototype.getFilteredBagTiddlers = function (bag_name, searchTerm) {
return this.sqlTiddlerDatabase.getFilteredBagTiddlers(bag_name, searchTerm);
};
SqlTiddlerStore.prototype.dispatchEvent = function(type /*, args */) {
const self = this;
if(!this.eventOutstanding[type]) {
@ -144,50 +148,50 @@ SqlTiddlerStore.prototype.processOutgoingTiddler = function(tiddlerFields,tiddle
/*
*/
SqlTiddlerStore.prototype.processIncomingTiddler = function(tiddlerFields, existing_attachment_blob, existing_canonical_uri) {
let attachmentSizeLimit = $tw.utils.parseNumber(this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AttachmentSizeLimit"));
let attachmentSizeLimit = $tw.utils.parseNumber(this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/AttachmentSizeLimit"));
if(attachmentSizeLimit < 100 * 1024) {
attachmentSizeLimit = 100 * 1024;
}
const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes";
const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"];
const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64";
const attachmentsEnabled = this.adminWiki.getTiddlerText("$:/config/MultiWikiServer/EnableAttachments", "yes") === "yes";
const contentTypeInfo = $tw.config.contentTypeInfo[tiddlerFields.type || "text/vnd.tiddlywiki"];
const isBinary = !!contentTypeInfo && contentTypeInfo.encoding === "base64";
let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit;
let shouldProcessAttachment = tiddlerFields.text && tiddlerFields.text.length > attachmentSizeLimit;
if(existing_attachment_blob) {
const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob);
if(fileSize <= attachmentSizeLimit) {
const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob);
const hasCanonicalField = !!tiddlerFields._canonical_uri;
const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri));
shouldProcessAttachment = !skipAttachment;
} else {
shouldProcessAttachment = false;
}
}
if(existing_attachment_blob) {
const fileSize = this.attachmentStore.getAttachmentFileSize(existing_attachment_blob);
if(fileSize <= attachmentSizeLimit) {
const existingAttachmentMeta = this.attachmentStore.getAttachmentMetadata(existing_attachment_blob);
const hasCanonicalField = !!tiddlerFields._canonical_uri;
const skipAttachment = hasCanonicalField && (tiddlerFields._canonical_uri === (existingAttachmentMeta ? existingAttachmentMeta._canonical_uri : existing_canonical_uri));
shouldProcessAttachment = !skipAttachment;
} else {
shouldProcessAttachment = false;
}
}
if(attachmentsEnabled && isBinary && shouldProcessAttachment) {
const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({
text: tiddlerFields.text,
type: tiddlerFields.type,
reference: tiddlerFields.title,
_canonical_uri: tiddlerFields._canonical_uri
});
if(tiddlerFields && tiddlerFields._canonical_uri) {
delete tiddlerFields._canonical_uri;
}
return {
tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }),
attachment_blob: attachment_blob
};
} else {
return {
tiddlerFields: tiddlerFields,
attachment_blob: existing_attachment_blob
};
}
if(attachmentsEnabled && isBinary && shouldProcessAttachment) {
const attachment_blob = existing_attachment_blob || this.attachmentStore.saveAttachment({
text: tiddlerFields.text,
type: tiddlerFields.type,
reference: tiddlerFields.title,
_canonical_uri: tiddlerFields._canonical_uri
});
if(tiddlerFields && tiddlerFields._canonical_uri) {
delete tiddlerFields._canonical_uri;
}
return {
tiddlerFields: Object.assign({}, tiddlerFields, { text: undefined }),
attachment_blob: attachment_blob
};
} else {
return {
tiddlerFields: tiddlerFields,
attachment_blob: existing_attachment_blob
};
}
};
SqlTiddlerStore.prototype.saveTiddlersFromPath = function(tiddler_files_path,bag_name) {

View File

@ -6,6 +6,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-bag
width="32px"
/> Bag <$text text={{{ [<bag-name>]}}}/>
<!-- Import form for Tiddlers -->
<form
method="post"
action="tiddlers/"
@ -35,11 +36,175 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/get-bag
</form>
<ul>
<$list filter="[<bag-titles>jsonget[]sort[]]">
<li>
<a href=`/bags/${ [<bag-name>encodeuricomponent[]] }$/tiddlers/${ [<currentTiddler>encodeuricomponent[]] }$` rel="noopener noreferrer" target="_blank">
<$text text=<<currentTiddler>>/>
</a>
</li>
</$list>
</ul>
<!-- Filter input for list-->
<div class="filter-section">
<form method="GET">
<label>Filter tiddlers:</label>
<select name="field">
<option value="title" selected={{{ [<current-field>match[title]then[selected]] }}}>Title</option>
<option value="tag" selected={{{ [<current-field>match[tag]then[selected]] }}}>Tags</option>
<option value="text" selected={{{ [<current-field>match[text]then[selected]] }}}>Text</option>
</select>
<input type="text" name="filter" value=<<current-filter>> placeholder="Enter search term..."/>
<input type="submit" value="Filter"/>
<a href={{{ [<bag-name>addprefix[/bags/]addsuffix[/]] }}} class="button">Show All</a>
</form>
</div>
<!-- Tiddler List with Checkboxes -->
<form action="tiddlers/" method="post" class="mws-form" enctype="multipart/form-data">
<div class="mws-form-heading">
Move Selected Tiddlers
</div>
<div class="mws-form-fields">
<!-- Hidden operation field -->
<input type="hidden" name="operation" value="move"/>
<!-- Hidden source bag field -->
<input type="hidden" name="source-bag" value=<<bag-name>>/>
<!-- Tiddler List with Checkboxes -->
<div class="tiddler-list">
<$list filter="[<bag-titles>jsonget[]sort[]]">
<div class="tiddler-entry">
<$set name="tidID" value={{{ [<currentTiddler>encodeuricomponent[]] }}}>
<label for="chk_<<tidID>>">
<$link to=<<currentTiddler>>><<currentTiddler>></$link>
</label>
</$set>
</div>
</$list>
</div>
<style>
.bag-selector {
margin: 15px 0;
padding: 15px;
background-color: #f8f8f8;
border-radius: 4px;
}
.bag-selector label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.bag-selector select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
background-color: #2d91ec;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
button:hover {
background-color: #1a7fd1;
}
button:active {
background-color: #166bb0;
}
.deselect-all {
margin-left: 10px;
background-color: #6c757d;
}
.filter-section {
margin-bottom: 15px;
display: flex;
gap: 10px;
}
.filter-section .button {
display: inline-block;
padding: 8px 16px;
background-color: #6c757d;
color: white;
text-decoration: none;
border-radius: 4px;
border: none;
}
.filter-section .button:hover {
background-color: #5a6268;
}
.filter-section input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.filter-section select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.move-selected:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.mws-form-field input[readonly] {
background-color: #f5f5f5;
cursor: not-allowed;
}
.select-all {
margin: 15px 0;
padding: 10px;
background-color: #f8f8f8;
border-radius: 4px;
}
.selection-controls {
margin: 10px 0;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.selection-controls button {
margin-right: 10px;
}
#selection-count {
font-weight: bold;
color: #2d91ec;
}
.tiddler-entry {
display: flex;
align-items: center;
padding: 5px;
border-bottom: 1px solid #eee;
}
.tiddler-entry:hover {
background-color: #f5f5f5;
}
.tiddler-entry input[type="checkbox"] {
margin-right: 10px;
}
</style>