feat(fase-11): settings tag management subsection (11.3.6)
Adds a "Tags" section to /settings between Collectives and Account. Lists every tag in the active collective with: * inline rename — tap the chip, edit, Enter / blur to save * recolor swatch row (8 preset dots, current colour outlined) * delete button per tag (cascades item attachments via FK) Uses the tagsStore which is already subscribed to item_tags realtime, so edits from another tab / device update this view live without a reload. No new screens; everything is inline in the existing settings layout. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,15 @@
|
|||||||
import ImageCropper from '$lib/components/ImageCropper.svelte';
|
import ImageCropper from '$lib/components/ImageCropper.svelte';
|
||||||
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||||
import SyncConflictsPanel from '$lib/components/SyncConflictsPanel.svelte';
|
import SyncConflictsPanel from '$lib/components/SyncConflictsPanel.svelte';
|
||||||
|
import TagChip from '$lib/components/TagChip.svelte';
|
||||||
|
import {
|
||||||
|
tags as tagsStore,
|
||||||
|
loadTags,
|
||||||
|
renameTag,
|
||||||
|
recolorTag,
|
||||||
|
deleteTag
|
||||||
|
} from '$lib/stores/tags';
|
||||||
|
import type { ItemTag, ItemTagColor } from '@colectivo/types';
|
||||||
import { languageTag, setLanguageTag } from '$lib/paraglide/runtime';
|
import { languageTag, setLanguageTag } from '$lib/paraglide/runtime';
|
||||||
import * as m from '$lib/paraglide/messages';
|
import * as m from '$lib/paraglide/messages';
|
||||||
import type { ThemePreference } from '$lib/theme';
|
import type { ThemePreference } from '$lib/theme';
|
||||||
@@ -43,8 +52,54 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadProfile();
|
await loadProfile();
|
||||||
|
if ($currentCollective) {
|
||||||
|
await loadTags($currentCollective.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Inline edit state for the tag list (one row at a time).
|
||||||
|
let editingTagId = $state<string | null>(null);
|
||||||
|
let editingTagName = $state('');
|
||||||
|
const TAG_COLORS: ItemTagColor[] = [
|
||||||
|
'slate',
|
||||||
|
'red',
|
||||||
|
'amber',
|
||||||
|
'green',
|
||||||
|
'sky',
|
||||||
|
'indigo',
|
||||||
|
'pink',
|
||||||
|
'stone'
|
||||||
|
];
|
||||||
|
|
||||||
|
function startEditTag(tag: ItemTag) {
|
||||||
|
editingTagId = tag.id;
|
||||||
|
editingTagName = tag.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commitEditTag() {
|
||||||
|
const id = editingTagId;
|
||||||
|
const next = editingTagName.trim();
|
||||||
|
editingTagId = null;
|
||||||
|
editingTagName = '';
|
||||||
|
if (id && next) {
|
||||||
|
await renameTag(id, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEditTag() {
|
||||||
|
editingTagId = null;
|
||||||
|
editingTagName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRecolor(tag: ItemTag, color: ItemTagColor) {
|
||||||
|
if (tag.color === color) return;
|
||||||
|
await recolorTag(tag.id, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteTag(id: string) {
|
||||||
|
await deleteTag(id);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadProfile() {
|
async function loadProfile() {
|
||||||
if (!$currentUser) return;
|
if (!$currentUser) return;
|
||||||
|
|
||||||
@@ -397,6 +452,68 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Tags (Fase 11.3.6) — collective-scoped tag management. Lives in
|
||||||
|
settings because the tag flow on the list view is per-item; the
|
||||||
|
full list of tags + rename + recolor + delete is admin-style. -->
|
||||||
|
<section data-testid="settings-tags-section" class="pt-8">
|
||||||
|
<p class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
|
{m.settings_tags_section()}
|
||||||
|
</p>
|
||||||
|
{#if $tagsStore.length === 0}
|
||||||
|
<p class="text-sm text-text-secondary">{m.settings_tags_empty()}</p>
|
||||||
|
{:else}
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{#each $tagsStore as tag (tag.id)}
|
||||||
|
<li
|
||||||
|
data-testid="settings-tag-row"
|
||||||
|
data-tag={tag.name}
|
||||||
|
class="flex items-center gap-3 rounded-md bg-surface-raised px-3 py-2"
|
||||||
|
>
|
||||||
|
{#if editingTagId === tag.id}
|
||||||
|
<input
|
||||||
|
bind:value={editingTagName}
|
||||||
|
onblur={commitEditTag}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
void commitEditTag();
|
||||||
|
} else if (e.key === 'Escape') cancelEditTag();
|
||||||
|
}}
|
||||||
|
class="min-w-0 flex-1 rounded bg-background px-2 py-1 text-sm text-text-primary outline-none ring-1 ring-black/10 focus:ring-slate-900 dark:ring-white/10 dark:focus:ring-slate-100"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => startEditTag(tag)}
|
||||||
|
class="min-w-0 flex-1 text-left"
|
||||||
|
>
|
||||||
|
<TagChip name={tag.name} color={tag.color} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<div class="flex shrink-0 items-center gap-1" aria-label={m.settings_tags_color()}>
|
||||||
|
{#each TAG_COLORS as color}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => handleRecolor(tag, color)}
|
||||||
|
aria-label={color}
|
||||||
|
class="h-4 w-4 rounded-full bg-{color}-500 {tag.color === color ? 'ring-2 ring-offset-1 ring-slate-700 dark:ring-slate-300' : 'opacity-70 hover:opacity-100'}"
|
||||||
|
></button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="settings-tag-delete"
|
||||||
|
onclick={() => handleDeleteTag(tag.id)}
|
||||||
|
class="rounded-md border border-slate-300 px-2 py-1 text-xs font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
{m.settings_tags_delete()}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Account — spacing creates the separation, not a border line -->
|
<!-- Account — spacing creates the separation, not a border line -->
|
||||||
<section class="pt-8">
|
<section class="pt-8">
|
||||||
<p class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
<p class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||||
|
|||||||
Reference in New Issue
Block a user