From b61ce5c8ad55de5c43237e7fff0cbef352f0cf9a Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Sun, 25 Oct 2026 18:22:48 +0100 Subject: [PATCH] =?UTF-8?q?Ic=C3=B4ne=20de=20l'arbre=20(un=20emoji,=20dans?= =?UTF-8?q?=20les=20menus,=20le=20compte=20et=20la=20page=20d'invitation)?= =?UTF-8?q?=20et=20bouton=20=C2=AB=20Modifier=20=C2=BB=20pour=20qui=20g?= =?UTF-8?q?=C3=A8re=20l'arbre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++++ .../20261025_002_icone_arbre/migration.sql | 2 ++ backend/prisma/schema.prisma | 2 ++ backend/src/lib/tree-invitations.ts | 2 +- backend/src/routes/managers.ts | 1 + backend/src/routes/trees.ts | 10 ++++++++ backend/test/co-gestion.db.test.ts | 10 +++++++- frontend/src/App.jsx | 1 + frontend/src/components/AccountDashboard.jsx | 2 +- frontend/src/components/AccountScreens.css | 4 +++ frontend/src/components/AdminPanel.jsx | 25 +++++++++++++++++++ frontend/src/components/InvitationScreen.jsx | 1 + frontend/src/components/SettingsPanels.css | 7 ++++++ frontend/src/components/navbar/AvatarMenu.css | 3 +++ frontend/src/components/navbar/AvatarMenu.jsx | 2 +- .../components/navbar/ContextualNavbar.jsx | 9 ++++--- frontend/src/i18n/en/gestion.js | 5 ++++ 17 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 backend/prisma/migrations/20261025_002_icone_arbre/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index b152932..788ab90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ Notable changes to Parenthèse. Dates are in YYYY-MM-DD format. - Deleting your account now names the co-managers of the trees you own, which are deleted for them too. - In the tree menu, "Admin" is now "Co-manager". +### Trees + +- A tree can have an icon, picked from fifteen emoji in Settings > Your tree. It replaces the first letter in the tree menu, on the account page and on the invitation page. +- For whoever manages the tree, the button at the bottom right now reads Edit, with a pencil. The family keeps Add. +- A tree name starting with an emoji no longer shows a broken letter in the menus. + ### Fixes - An admin can switch a person's profile to edit mode from a new Edit button on the profile. diff --git a/backend/prisma/migrations/20261025_002_icone_arbre/migration.sql b/backend/prisma/migrations/20261025_002_icone_arbre/migration.sql new file mode 100644 index 0000000..a8953e0 --- /dev/null +++ b/backend/prisma/migrations/20261025_002_icone_arbre/migration.sql @@ -0,0 +1,2 @@ +-- Icône d'un arbre (un emoji), facultative. Migration additive : colonne vide par défaut. +ALTER TABLE "trees" ADD COLUMN "icon" TEXT; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 6d7fdaa..581350e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -126,6 +126,8 @@ model Tree { slug String @unique name String description String? + // Icône choisie pour l'arbre (un emoji), à la place de la première lettre dans les menus ; null = la lettre + icon String? rootPersonId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/backend/src/lib/tree-invitations.ts b/backend/src/lib/tree-invitations.ts index 4c4a986..376545f 100644 --- a/backend/src/lib/tree-invitations.ts +++ b/backend/src/lib/tree-invitations.ts @@ -37,7 +37,7 @@ export async function findPendingInvitationByToken(db: Db, token: string) { if (!token || token.length > 200) return null return db.treeInvitation.findFirst({ where: { tokenHash: hashInvitationToken(token), ...pendingInvitationWhere() }, - include: { tree: { select: { id: true, name: true, ownerUserId: true, owner: { select: { firstName: true } } } } }, + include: { tree: { select: { id: true, name: true, icon: true, ownerUserId: true, owner: { select: { firstName: true } } } } }, }) } diff --git a/backend/src/routes/managers.ts b/backend/src/routes/managers.ts index 1f88080..94ae651 100644 --- a/backend/src/routes/managers.ts +++ b/backend/src/routes/managers.ts @@ -210,6 +210,7 @@ export const managerRoutes: FastifyPluginAsync = async (app) => { } return reply.send({ treeName: invitation.tree.name, + treeIcon: invitation.tree.icon, invitedBy: invitation.tree.owner.firstName || null, emailHint: maskEmail(invitation.email), expiresAt: invitation.expiresAt, diff --git a/backend/src/routes/trees.ts b/backend/src/routes/trees.ts index b3f4009..e461f31 100644 --- a/backend/src/routes/trees.ts +++ b/backend/src/routes/trees.ts @@ -40,6 +40,8 @@ const patchTreeSchema = z slug: z.string().min(1).max(128).optional(), description: z.string().max(500).optional().nullable(), rootPersonId: z.string().min(1).optional().nullable(), + // Un emoji (graphème composé compris), ou null pour revenir à la première lettre + icon: z.string().trim().max(16).regex(/^\p{Extended_Pictographic}/u).optional().nullable(), }) .refine((value) => Object.keys(value).length > 0, { message: 'at_least_one_field_required', @@ -392,6 +394,7 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { slug: true, name: true, description: true, + icon: true, rootPersonId: true, createdAt: true, updatedAt: true, @@ -413,6 +416,7 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { slug: true, name: true, description: true, + icon: true, rootPersonId: true, createdAt: true, updatedAt: true, @@ -442,6 +446,7 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { slug: string name: string description: string | null + icon: string | null rootPersonId: string | null createdAt: Date updatedAt: Date @@ -665,6 +670,7 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { name?: string slug?: string description?: string | null + icon?: string | null rootPersonId?: string | null } = {} @@ -693,6 +699,10 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { updateData.description = payload.data.description } + if (payload.data.icon !== undefined) { + updateData.icon = payload.data.icon + } + if (payload.data.rootPersonId !== undefined) { if (payload.data.rootPersonId === null) { updateData.rootPersonId = null diff --git a/backend/test/co-gestion.db.test.ts b/backend/test/co-gestion.db.test.ts index a73b778..14e2435 100644 --- a/backend/test/co-gestion.db.test.ts +++ b/backend/test/co-gestion.db.test.ts @@ -2,7 +2,7 @@ import type { FastifyInstance } from 'fastify' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' // Co-gestion d'un arbre (#145), sur une vraie base : une fonction de droits se vérifie avec de vraies requêtes, -// pas avec des mocks. Lancé seulement avec TEST_DATABASE_URL (base jetable, migrée) : +// pas avec des mocks. Lancé seulement avec TEST_DATABASE_URL (base jetable, migrée, en UTF-8 pour les emoji) : // TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/genealogy_test npx vitest run test/co-gestion.db.test.ts const TEST_DB = process.env.TEST_DATABASE_URL @@ -206,6 +206,14 @@ describe.skipIf(!TEST_DB)('co-gestion, vraie base', () => { expect((await call('GET', `/trees/${treeId}/graph`, co.token)).status).toBe(403) }) + it("l'icône de l'arbre : un emoji ou rien, posée par un gestionnaire, rendue dans la liste des arbres", async () => { + expect((await call('PATCH', `/trees/${treeId}`, owner.token, { icon: 'abc' })).status).toBe(400) + expect((await call('PATCH', `/trees/${treeId}`, owner.token, { icon: '🌳' })).status).toBe(200) + const list = await call('GET', '/trees', owner.token) + expect(list.body.trees.find((t: { id: string }) => t.id === treeId).icon).toBe('🌳') + expect((await call('PATCH', `/trees/${treeId}`, owner.token, { icon: null })).status).toBe(200) + }) + describe('OIDC', () => { let address = 0 async function oidcLogin(identity: { subject: string; email: string; emailVerified: boolean }) { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b4b0059..ee026d5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3382,6 +3382,7 @@ function App() { onTreeDeleted={handleTreeDeleted} onLeftTree={handleTreeDeleted} onOpenShare={() => { setAdminPanelVisible(false); handleOpenContributionPanel() }} + onTreeUpdated={() => { if (auth.userAuth.token) auth.loadAccountTrees(auth.userAuth.token).catch(() => {}) }} onGedcomImported={handleGedcomImported} onOpenPhotoStart={() => { setAdminPanelVisible(false); openPhotoStart() }} onClose={() => setAdminPanelVisible(false)} diff --git a/frontend/src/components/AccountDashboard.jsx b/frontend/src/components/AccountDashboard.jsx index 7f693ec..2ea95b8 100644 --- a/frontend/src/components/AccountDashboard.jsx +++ b/frontend/src/components/AccountDashboard.jsx @@ -279,7 +279,7 @@ const AccountDashboard = ({ className={`tree-item pz-tree-item ${isActive ? 'active' : ''}`} onClick={() => onOpenTree(tree.id)} > - + {tree.name} {isActive && {t('Ouvert')}}