diff --git a/frontend/src/lib/components/tag/TagRuleEditor.svelte b/frontend/src/lib/components/tag/TagRuleEditor.svelte index 3638237..a27438d 100644 --- a/frontend/src/lib/components/tag/TagRuleEditor.svelte +++ b/frontend/src/lib/components/tag/TagRuleEditor.svelte @@ -57,6 +57,27 @@ } } + async function toggleRule(rule: TagRule) { + if (busy) return; + busy = true; + error = ''; + const thenTagId = rule.then_tag_id!; + const newActive = !rule.is_active; + try { + await api.delete(`/tags/${tagId}/rules/${thenTagId}`); + const updated = await api.post(`/tags/${tagId}/rules`, { + then_tag_id: thenTagId, + is_active: newActive, + apply_to_existing: false, + }); + onRulesChange(rules.map((r) => r.then_tag_id === thenTagId ? updated : r)); + } catch (e) { + error = e instanceof ApiError ? e.message : 'Failed to update rule'; + } finally { + busy = false; + } + } + async function removeRule(thenTagId: string) { if (busy) return; busy = true; @@ -86,12 +107,30 @@
{#each rules as rule (rule.then_tag_id)} {@const t = tagForId(rule.then_tag_id)} -
+
{#if t} {:else} {rule.then_tag_name ?? rule.then_tag_id} {/if} + + {/if} +
+
+ {#each filteredTags as t (t.id)} + addRule(t.id!)} /> + {:else} + {search.trim() ? 'No matching tags' : 'All tags already added'} + {/each} +
@@ -157,6 +203,31 @@ gap: 2px; } + .rule-row.inactive { + opacity: 0.45; + } + + .toggle-btn { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--color-text-muted); + cursor: pointer; + padding: 2px 3px; + border-radius: 3px; + line-height: 1; + } + + .toggle-btn.active { + color: var(--color-accent); + } + + .toggle-btn:hover { + color: var(--color-text-primary); + } + .remove-btn { background: none; border: none; @@ -193,6 +264,12 @@ gap: 6px; } + .search-wrap { + position: relative; + display: flex; + align-items: center; + } + .search { width: 100%; box-sizing: border-box; @@ -211,6 +288,27 @@ border-color: var(--color-accent); } + .search-clear { + position: absolute; + right: 6px; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + border: none; + background: none; + color: var(--color-text-muted); + cursor: pointer; + padding: 0; + } + + .search-clear:hover { + color: var(--color-text-primary); + background-color: color-mix(in srgb, var(--color-accent) 20%, transparent); + } + .tag-pick { display: flex; flex-wrap: wrap; diff --git a/frontend/vite-mock-plugin.ts b/frontend/vite-mock-plugin.ts index d2eef30..4d087c2 100644 --- a/frontend/vite-mock-plugin.ts +++ b/frontend/vite-mock-plugin.ts @@ -183,8 +183,8 @@ const mockTagsArr: MockTag[] = TAG_NAMES.map((name, i) => { // Backwards-compatible reference for existing file-tag lookups const MOCK_TAGS = mockTagsArr; -// Tag rules: Map> -const tagRules = new Map>(); +// Tag rules: Map> +const tagRules = new Map>(); // Mutable in-memory state for file metadata and tags const fileOverrides = new Map>(); @@ -381,10 +381,10 @@ export function mockApiPlugin(): Plugin { const tagRulesGetMatch = path.match(/^\/tags\/([^/]+)\/rules$/); if (method === 'GET' && tagRulesGetMatch) { const tid = tagRulesGetMatch[1]; - const ruleIds = [...(tagRules.get(tid) ?? new Set())]; - const items = ruleIds.map((thenId) => { + const ruleMap = tagRules.get(tid) ?? new Map(); + const items = [...ruleMap.entries()].map(([thenId, isActive]) => { const t = MOCK_TAGS.find((x) => x.id === thenId); - return { tag_id: tid, then_tag_id: thenId, then_tag_name: t?.name ?? null, is_active: true }; + return { tag_id: tid, then_tag_id: thenId, then_tag_name: t?.name ?? null, is_active: isActive }; }); return json(res, 200, items); } @@ -395,10 +395,11 @@ export function mockApiPlugin(): Plugin { const tid = tagRulesPostMatch[1]; const body = (await readBody(req)) as Record; const thenId = body.then_tag_id as string; - if (!tagRules.has(tid)) tagRules.set(tid, new Set()); - tagRules.get(tid)!.add(thenId); + const isActive = body.is_active !== false; + if (!tagRules.has(tid)) tagRules.set(tid, new Map()); + tagRules.get(tid)!.set(thenId, isActive); const t = MOCK_TAGS.find((x) => x.id === thenId); - return json(res, 201, { tag_id: tid, then_tag_id: thenId, then_tag_name: t?.name ?? null, is_active: true }); + return json(res, 201, { tag_id: tid, then_tag_id: thenId, then_tag_name: t?.name ?? null, is_active: isActive }); } // DELETE /tags/{id}/rules/{then_id}