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:
parent
bc6e58d0af
commit
6f3e70c5d6
@ -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) {
|
||||
}
|
||||
};
|
||||
|
||||
}());
|
||||
}());
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user