From a59e7e15ea492ebd0533757be8ae149907fd2b7a Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 18 May 2026 04:10:28 +0200 Subject: [PATCH] feat(fase-12): settings + manage toggles per section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new sections, both hooked into the precedence-aware features store: * /settings — "Visible sections" block with one toggle per SectionKey bound to setUserFeature(). When a collective-level override is present the toggle is disabled and a one-line "Hidden by the collective" hint replaces the visible state — making it obvious why the per-user toggle is moot. * /collective/manage — "Collective sections" block (admin-only, inside the existing isAdmin guard) with one toggle per SectionKey bound to setCollectiveFeature(). Flipping any of them propagates to every member in realtime via the publication added in the previous commit. Both helpers fall back to the default-ON semantics of section_enabled when the JSONB key is missing, so untouched rows render exactly as before fase 12. Co-Authored-By: Claude Opus 4.7 --- .../(app)/collective/manage/+page.svelte | 57 ++++++++++++++ .../src/routes/(app)/settings/+page.svelte | 74 ++++++++++++++++++- 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/apps/web/src/routes/(app)/collective/manage/+page.svelte b/apps/web/src/routes/(app)/collective/manage/+page.svelte index 23075e0..a45ecca 100644 --- a/apps/web/src/routes/(app)/collective/manage/+page.svelte +++ b/apps/web/src/routes/(app)/collective/manage/+page.svelte @@ -5,6 +5,9 @@ import { currentUser } from '$lib/stores/auth'; import { currentCollective, collectiveMembers, userCollectives } from '$lib/stores/collective'; import Avatar from '$lib/components/Avatar.svelte'; + import type { FeatureFlags, SectionKey } from '@colectivo/types'; + import { SECTION_KEYS } from '@colectivo/types'; + import { setCollectiveFeature } from '$lib/stores/features'; import * as m from '$lib/paraglide/messages'; type Member = { @@ -236,6 +239,31 @@ userCollectives.update((list) => list.map((it) => (it.id === c.id ? c : it))); } + // ── Fase 12.4.2 — collective section visibility (admin only) ──────────── + function sectionLabel(section: SectionKey): string { + if (section === 'lists') return m.section_label_lists(); + if (section === 'tasks') return m.section_label_tasks(); + if (section === 'notes') return m.section_label_notes(); + return m.section_label_search(); + } + + function collectiveFlag(section: SectionKey): boolean { + const flags = (($currentCollective?.feature_flags ?? {}) as FeatureFlags); + // Missing key → ON (the collective has "no opinion" and the user layer + // can take over). Equivalent to section_enabled's collective coalesce. + return flags[section] ?? true; + } + + async function toggleCollectiveSection(section: SectionKey) { + if (!isAdmin) return; + const next = !collectiveFlag(section); + try { + await setCollectiveFeature(section, next); + } catch { + // optimistic mutation already rolled back inside the helper + } + } + // ── Fase 10.3: dissolve collective (CU-H08) ───────────────────────────── let dissolveOpen = $state(false); let dissolveConfirmInput = $state(''); @@ -511,6 +539,35 @@ {/if} + +
+

+ {m.collective_section_visibility_title()} +

+

+ {m.collective_section_visibility_blurb()} +

+
    + {#each SECTION_KEYS as section (section)} + {@const enabled = collectiveFlag(section)} +
  • +

    {sectionLabel(section)}

    + +
  • + {/each} +
+
+

diff --git a/apps/web/src/routes/(app)/settings/+page.svelte b/apps/web/src/routes/(app)/settings/+page.svelte index e0dc28f..44b7c62 100644 --- a/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/web/src/routes/(app)/settings/+page.svelte @@ -17,7 +17,12 @@ recolorTag, deleteTag } from '$lib/stores/tags'; - import type { ItemTag, ItemTagColor } from '@colectivo/types'; + import type { ItemTag, ItemTagColor, FeatureFlags, SectionKey } from '@colectivo/types'; + import { SECTION_KEYS } from '@colectivo/types'; + import { + currentUserFeatures, + setUserFeature + } from '$lib/stores/features'; import { languageTag, setLanguageTag } from '$lib/paraglide/runtime'; import * as m from '$lib/paraglide/messages'; import type { ThemePreference } from '$lib/theme'; @@ -205,6 +210,39 @@ .eq('id', $currentUser.id); } + // ── Fase 12.4.1 — section visibility toggles ──────────────────────────── + // The "effective" map is derived from the user + active-collective flags + // (see $lib/stores/features). The disabled state of each toggle reflects + // whether the collective has explicitly hidden the section — when it has, + // the user toggle is moot and we surface that with a one-line hint. + function sectionLabel(section: SectionKey): string { + if (section === 'lists') return m.section_label_lists(); + if (section === 'tasks') return m.section_label_tasks(); + if (section === 'notes') return m.section_label_notes(); + return m.section_label_search(); + } + + function userFlag(section: SectionKey): boolean { + const flags = ($currentUserFeatures?.feature_flags ?? {}) as FeatureFlags; + const v = flags[section]; + // Default ON when the key is missing or null — matches section_enabled(). + return v ?? true; + } + + function collectiveHidden(section: SectionKey): boolean { + const flags = ($currentCollective?.feature_flags ?? {}) as FeatureFlags; + return flags[section] === false; + } + + async function toggleUserSection(section: SectionKey) { + const next = !userFlag(section); + try { + await setUserFeature(section, next); + } catch { + // optimistic mutation already rolled back inside the helper + } + } + async function switchLanguage(lang: 'en' | 'es') { language = lang; setLanguageTag(lang); @@ -420,6 +458,40 @@

+ +
+

+ {m.settings_section_visibility_title()} +

+

{m.settings_section_visibility_blurb()}

+
    + {#each SECTION_KEYS as section (section)} + {@const overridden = collectiveHidden(section)} + {@const checked = !overridden && userFlag(section)} +
  • +
    +

    {sectionLabel(section)}

    + {#if overridden} +

    {m.settings_section_visibility_overridden()}

    + {/if} +
    + +
  • + {/each} +
+
+