Four planned-fase docs for the first tagged release.
- Fase 17 — semver git tags + CHANGELOG.md (Keep-a-Changelog, [new]/
[fix]/[tweak] prefixes), one big v0.0.0-beta backfill block covering
Fases 0–16, About-modal in /settings rendering bundled CHANGELOG via
?raw import, deploy-script tag check + tag column in .deploys.log.
- Fase 18 — shopping list flow: required title (client + DB tighten),
per-collective per-prefix #N auto-numbering parsed from typed title,
collectives.default_list_title, collective_list_titles catalog + admin
RPCs, autocomplete union (catalog + last-10). Migration 027.
- Fase 19 — EMOJI_CATALOG with faces/food/animals/objects categories
(~280 codepoints, Unicode 14 baseline for iOS<17 compat), new
EmojiPicker.svelte replacing the 8-emoji hardcoded selector at 4
sites. No migration.
- Fase 20 — Euskera locale: messages/eu.json via machine-translate
seed flagged with [needs review], users.language check constraint
extended to ('en','es','eu'), accept-language parser + selector in
/settings. Migration 028. v1.0.0 tag lands at the end of this fase.
README + CLAUDE.md updated with the v1.0 cycle pointers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 KiB
10 KiB
Fase 18 — Shopping list creation flow (required title + numbered prefix + autocomplete + defaults)
v1.0 · 2/4. Segunda fase del ciclo v1.0.
Estado: 📋 Planificada. Cuatro asks acoplados sobre el momento "creo una lista nueva":
- Título requerido — no permitir crear sin nombre. Cliente bloquea + DB tightens.
- Auto-numerado
#N— si el título escrito coincide con un prefijo usado en el colectivo (ej.Compra), sugerirCompra #NconN = max(N) + 1. El#Nes parte del título (camponame), no columna separada. - Default list title — admin del colectivo configura un título por defecto que prefilla el input del modal create-list.
- Catálogo de títulos — admin gestiona una tabla de títulos sugeridos (CRUD inline en
/collective/manage); aparecen en el autocomplete junto a los últimos 10 títulos reales.
Decisiones clave:
shopping_lists.nameya esNOT NULL(migración 005). Falta enforce mínimo en cliente + check de longitud > 0 con trim.- Nuevo
collectives.default_list_title text(nullable). Vacío = no prefill. - Nuevo
collective_list_titles (collective_id, title text, primary key)— mirror del shape deitem_frequency. Solo admin escribe via RPC. - Autocomplete: union de (a)
collective_list_titles+ (b) últimos 10namedistintos deshopping_lists(mismo colectivo). Deduplicate case-insensitive. #Nparsing: regex^(.+?)\s*(?:#(\d+))?\s*$. Prefijo = grupo 1. Si el usuario escribeCompra, miramosname ILIKE 'Compra %' OR name ILIKE 'Compra'y extraemos el#Nmaximal. SugerimosCompra #N+1. Si el usuario escribeCompra #3explícitamente, no auto-bump.- Numbering scope = per-collective per-prefix (case-insensitive prefix match).
18.1 Modelo
Fichero: supabase/migrations/027_list_title_catalog.sql.
-- Default per-collective title (single string, nullable).
alter table public.collectives
add column default_list_title text;
-- Catalog of admin-curated suggested titles.
create table public.collective_list_titles (
collective_id uuid not null references public.collectives(id) on delete cascade,
title text not null,
created_at timestamptz not null default now(),
primary key (collective_id, title)
);
alter table public.collective_list_titles enable row level security;
-- Members read; only RPC writes.
create policy "select_member" on public.collective_list_titles
for select using (public.is_member(collective_id));
create policy "deny_direct_writes" on public.collective_list_titles
for all using (false) with check (false);
-- Admin-only RPCs.
create or replace function public.add_list_title(
p_collective_id uuid, p_title text
) returns void
language plpgsql security definer
set search_path = public, auth
as $$
declare
v_role text;
v_norm text := trim(p_title);
begin
if auth.uid() is null then raise exception 'not authenticated' using errcode = 'P0001'; end if;
if v_norm = '' then raise exception 'title cannot be empty' using errcode = 'P0001'; end if;
select role into v_role from public.collective_members
where collective_id = p_collective_id and user_id = auth.uid();
if v_role is distinct from 'admin' then
raise exception 'only admins can curate list titles' using errcode = 'P0002';
end if;
insert into public.collective_list_titles (collective_id, title)
values (p_collective_id, v_norm)
on conflict do nothing;
end; $$;
create or replace function public.remove_list_title(
p_collective_id uuid, p_title text
) returns void
language plpgsql security definer
set search_path = public, auth
as $$
declare v_role text;
begin
if auth.uid() is null then raise exception 'not authenticated' using errcode = 'P0001'; end if;
select role into v_role from public.collective_members
where collective_id = p_collective_id and user_id = auth.uid();
if v_role is distinct from 'admin' then
raise exception 'only admins can curate list titles' using errcode = 'P0002';
end if;
delete from public.collective_list_titles
where collective_id = p_collective_id and title = trim(p_title);
end; $$;
grant execute on function public.add_list_title(uuid, text) to authenticated;
grant execute on function public.remove_list_title(uuid, text) to authenticated;
Tareas:
- 18.1.1 Migración 027.
- 18.1.2 Tests pgTAP
supabase/tests/019_list_title_catalog.sql:- default_list_title default NULL, admin update OK, member update denied via RLS.
- add_list_title como admin OK, idempotente por unique.
- add_list_title como member → P0002.
- add_list_title con título vacío → P0001.
- remove_list_title como admin borra fila.
- remove_list_title como guest → P0002.
- 18.1.3
just db-types.
18.2 Stores
Fichero: nuevo apps/web/src/lib/stores/listTitles.ts, extender apps/web/src/lib/stores/lists.ts.
- 18.2.1 Store
listTitles:loadTitleCatalog(collectiveId)— select * fromcollective_list_titles.addTitle(collectiveId, title)/removeTitle(collectiveId, title)— RPCs.
- 18.2.2 Helper
fetchTitleSuggestions(collectiveId, prefix)— union catalog + últimos 10shopping_lists.namedistintos. Dedupe case-insensitive. Limit 15. - 18.2.3 Helper
computeNextNumber(collectiveId, basePrefix):- Query:
select name from shopping_lists where collective_id = $1 and name ILIKE $2 and deleted_at is null. - Pattern
$2 = basePrefix || ' #%'plusbasePrefixexact. - Parse trailing
#(\d+)con regex. Tomarmax(N) + 1. Si ninguno tiene número, devolver1solo si el prefijo bare existe (ya hay una sin número), de otro modonull.
- Query:
- 18.2.4 Tests integración
packages/test-utils/tests/list-title-flow.test.ts:- LT-INT-01 admin add → suggestions incluyen el título.
- LT-INT-02 member add → P0002.
- LT-INT-03 computeNextNumber con 3 listas
Compra #1,Compra #2,Compra #5→ devuelve 6. - LT-INT-04 computeNextNumber sin matches → null.
- 18.2.5 Tests unit
apps/web/src/lib/utils/list-title.test.ts(extraer la regex + numbering en un módulo puro):- LT-U-01 parse de "Compra #5" →
{ prefix: 'Compra', number: 5 }. - LT-U-02 parse de "Compra" →
{ prefix: 'Compra', number: null }. - LT-U-03 parse de "#5" (sin prefix) → no autonumera (returns null prefix).
- LT-U-04 nextNumber sobre lista de números
[1,2,5]→ 6. - LT-U-05 nextNumber sobre lista vacía → 1.
- LT-U-01 parse de "Compra #5" →
18.3 UI — create-list modal
Ficheros: ubicación actual de creación de lista (probablemente apps/web/src/routes/(app)/lists/+page.svelte o similar — auditar).
- 18.3.1 Input prefilled con
currentCollective.default_list_title || ''. - 18.3.2 Mientras el usuario escribe:
- Si el typed value matches un prefijo conocido + no incluye
#N, mostrar un chip discretoSugerencia: Compra #6(texto pequeño debajo del input, click rellena el input). - Suggestion list aparece como dropdown con
fetchTitleSuggestions(prefix). Mismo patrón queTagPicker(Fase 11).
- Si el typed value matches un prefijo conocido + no incluye
- 18.3.3 Submit deshabilitado si
name.trim() === ''. Sin spinner cuando deshabilitado. - 18.3.4 Si submit ocurre y el título matches un prefijo conocido sin
#NycomputeNextNumberdevuelve un valor, auto-suffix el#Nantes de insertar. Visible para el usuario en un toast no intrusivo: "Creada comoCompra #6".
Decisión UX: auto-suffix solo si el typed value coincide con un título del catálogo. Para prefijos libres (no en catalog), respetar lo que el usuario escribió tal cual.
18.4 UI — /collective/manage catálogo
Fichero: apps/web/src/routes/(app)/collective/manage/+page.svelte.
- 18.4.1 Nueva subsección "Títulos sugeridos" debajo de "Items frecuentes":
- Input
default_list_title(text input, single-line, save on blur). - Lista de títulos curados con botón "Borrar" por fila.
- Botón "Añadir título" + modal pequeño con input + Save.
- Input
- 18.4.2 Members ven la sección read-only (lista visible; sin acciones). Guests no la ven.
18.5 Tests E2E
- 18.5.1 Playwright
tests/e2e/list-title-flow.test.ts:- LTF-01 Ana setea
default_list_title = "Compra"en manage; crea lista nueva → input prefilled "Compra"; submit → lista creada como "Compra" (no#1aún porque no había prefix-match en catalog antes). - LTF-02 Ana añade "Compra" al catálogo; crea 3 listas; verifica que la 3ª se nombra "Compra #2" automáticamente (la 1ª = "Compra", 2ª = "Compra #1" si #1 ausente; ajustar según
computeNextNumberexact behavior). - LTF-03 Ana intenta crear con input vacío → botón disabled, no fire.
- LTF-04 Borja (member) en manage ve la subsección con acciones disabled.
- LTF-01 Ana setea
18.Z Verificación + cierre
- 18.Z.1
just test-allverde. Suma esperada: +6 pgTAP + 4 integración + 5 unit + 4 e2e = +19. - 18.Z.2 Smoke: crear 3 listas seguidas con prefijo común; verificar numbering correcto.
- 18.Z.3 Entrada en CHANGELOG:
[new] Required list titles + auto-numbered prefix + admin-curated title catalog (Fase 18). - 18.Z.4 Documentar en
docs/history/fase-18-shopping-list-flow.md.
Riesgos / notas
- Race condition en numbering: dos usuarios crean a la vez con prefijo
Compra. Ambos clientes resuelvencomputeNextNumber → 6. Resultado: dosCompra #6. Mitigación posible: unique constraint(collective_id, name)— pero rompe casos donde es deliberado tener duplicados. Aceptar el race; documentar. - Caso "el usuario quiere típo libre": si el typed value no matches un prefix conocido, no auto-suffix. Asegurar que el chip de sugerencia solo aparece para matches del catálogo.
- Default title sensible al colectivo activo: al cambiar de colectivo en la sidebar, el modal create-list debe re-leer
currentCollective.default_list_title. Reactivo vía store. - i18n: el
#Nformato es universal; no traducir. Strings nuevos:create_list_input_placeholder,create_list_suggestion_chip,create_list_auto_numbered_toast,manage_list_titles_section_title,manage_default_list_title_label.
Scope explícito fuera
- Plantillas de items (lista con items pre-cargados). Solo título.
- Numbering por mes/año (formato
Compra 2026-05). Solo#Ninteger. - Edición masiva del catálogo via CSV. Solo CRUD inline.
- Sugerencias cross-collective (títulos de otros colectivos del mismo usuario).