feat(podcasts): the rules page retires — the collection builder IS the filter builder (item 3.4)
The smart-collection editor at /collections/:id/rules now hosts the SAME builder island as All-Books (FilterBuilderRows — the rows markup extracted into one shared templ both mounts render; the shared filterBuilderExtra component). One vocabulary, one engine, one row UI; the collection differences are exactly three: the payload is the collection's scope (episode trees get the episode fields — books trees hide them), Apply writes the tree to the collection (PUT filter_tree through the validated write path), and a legacy rules array preloads as its CONVERTED tree (RulesToFilterTree rendered server-side) so the upgrade lands at the first Apply — read-compat never mutates data silently. Retired with the page: the per-rule CRUD endpoints and test-rules (their handler bodies, routes, api-docs rows, and the dead collections.ts island functions — the file's LIVE halves, the collection modal/navigate/ grid-refresh, stay; the dead rules half went). match-mode endpoint and preview survive (legacy channel + the dashboard custom-section-builder, flagged for its own migration). Contract-pin changelog documents the deletions per the asymmetry rule. The builder component gains collection mounts: scopeKey (the row universe for counts/apply), data-tree prefill, treeJSON (the apply wire with scope riding the top level), and an apply hook (the page owns the write; All-Books never sets one). ensureReadyRule defaults to the scope's first field instead of hardcoding author. Tests migrated onto trees: TestSmartCollections' live-membership / reject-mutations / include-auto / system-collection subtests now drive TREE collections; the deleted-endpoint subtests die with them.
This commit is contained in:
1 parent
eb68672390
commit
4f71eda6be
16 files changed
+518
-1422
No files matched your search
@@ -47,139 +47,6 @@ function initializeCollectionWebSocket(): void {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCollectionRules(collectionId: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/collections/${collectionId}/rules`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const rules: CollectionRule[] = await response.json();
|
||||
renderRules(rules);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load rules:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRules(rules: CollectionRule[]): void {
|
||||
const container = document.getElementById("rules-list");
|
||||
if (!container) return;
|
||||
|
||||
if (rules.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p class="text-center p-4" style="color: var(--text-secondary)">No rules defined</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = rules
|
||||
.map(
|
||||
(rule) => `
|
||||
<div class="p-3 rounded-lg border mb-2 flex justify-between items-center"
|
||||
style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">${rule.field} ${rule.operator} "${rule.value}"</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Priority: ${rule.priority} | ${rule.enabled ? "Enabled" : "Disabled"}</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="window.editRule('${rule.id}')" class="btn-secondary px-2 py-1 rounded text-sm">Edit</button>
|
||||
<button onclick="window.deleteRule('${rule.id}')" class="btn-secondary px-2 py-1 rounded text-sm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function createRule(collectionId: string, rule: Partial<CollectionRule>): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/collections/${collectionId}/rules`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(rule),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast("Rule created", "success");
|
||||
loadCollectionRules(collectionId);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.error || "Failed to create rule", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create rule:", error);
|
||||
showToast("Failed to create rule", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRule(collectionId: string, ruleId: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
if (!confirm("Are you sure you want to delete this rule?")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/collections/${collectionId}/rules/${ruleId}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast("Rule deleted", "success");
|
||||
loadCollectionRules(collectionId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete rule:", error);
|
||||
showToast("Failed to delete rule", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function testRule(collectionId: string, rule: Partial<CollectionRule>): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/collections/${collectionId}/rules/test`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(rule),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const results = await response.json();
|
||||
renderTestResults(results);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to test rule:", error);
|
||||
showToast("Failed to test rule", "error");
|
||||
}
|
||||
}
|
||||
|
||||
function renderTestResults(results: unknown[]): void {
|
||||
const container = document.getElementById("test-results");
|
||||
if (!container) return;
|
||||
|
||||
if (!results || (Array.isArray(results) && results.length === 0)) {
|
||||
container.innerHTML =
|
||||
'<p class="p-2 text-sm" style="color: var(--text-secondary)">No matching books found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `<p class="p-2 text-sm" style="color: var(--text-secondary)">${Array.isArray(results) ? results.length : 0} matching books</p>`;
|
||||
}
|
||||
|
||||
function setupHTMXAuth(): void {
|
||||
document.body.addEventListener("htmx:configRequest", function (evt: Event) {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -476,14 +343,11 @@ function filterCollectionBooks(): void {
|
||||
|
||||
export {
|
||||
closeCollectionModal,
|
||||
createRule,
|
||||
deleteRule,
|
||||
filterCollectionBooks,
|
||||
filterIcons,
|
||||
initColorSelection,
|
||||
initializeCollectionWebSocket,
|
||||
initCollectionsPage,
|
||||
loadCollectionRules,
|
||||
navigateToCollection,
|
||||
populateIconGrid,
|
||||
selectColor,
|
||||
@@ -491,21 +355,17 @@ export {
|
||||
setupHTMXAuth,
|
||||
setupHTMXModalInit,
|
||||
showAllIcons,
|
||||
testRule,
|
||||
toggleSelection,
|
||||
moveCollectionBook,
|
||||
};
|
||||
|
||||
Alpine.data("collections", () => ({
|
||||
closeCollectionModal,
|
||||
createRule,
|
||||
deleteRule,
|
||||
filterCollectionBooks,
|
||||
filterIcons,
|
||||
initColorSelection,
|
||||
initializeCollectionWebSocket,
|
||||
initCollectionsPage,
|
||||
loadCollectionRules,
|
||||
moveCollectionBook,
|
||||
navigateToCollection,
|
||||
populateIconGrid,
|
||||
@@ -514,7 +374,6 @@ Alpine.data("collections", () => ({
|
||||
selectedBooks: [] as string[],
|
||||
setupHTMXModalInit,
|
||||
showAllIcons,
|
||||
testRule,
|
||||
toggleSelection,
|
||||
|
||||
showConfirm: false,
|
||||
|
||||
Reference in new issue
Block a user