feat(share,permissions): partage de page par groupes (page_shares)
FlowDeck CI / lint (push) Failing after 1m10s
FlowDeck CI / test (push) Successful in 9m55s
FlowDeck CI / docker (push) Successful in 1m11s

- app/db.py: ajout colonne shared_with_group_id (FK user_groups, migration
  backfill v5.x) dans page_shares
- app/routers/sharing.py: POST /api/pages/{id}/share accepte group_id
  (upsert, verif FK groupe), GET /shares expose kind/group_name, synchro
  bidirectionnelle avec page_permissions (mirror grant/revoke) pour que
  PermissionManager donne un acces effectif (view/comment/edit) aux membres
  du groupe; PUT/DELETE gardent le miroir a jour
- app/routers/board.py, library.py: received/made incluent les partages via
  groupes (JOIN group_members)
- app/templates/_page_editor_content.html, _page_editor_scripts.html:
  dialogue Share — invite groups (fetch /api/v2/groups, filtre deja partages),
  pickInviteGroup, shareInvite(group_id), rendu accessList avec avatar groupe
- tests/test_share_groups.py: 10 tests (CRUD groupe, kind, miroir ACL)

Chore: inclut evolutions v6.4.0 deja en working copy (webhooks prod,
migrations, sync, config/main) pour garder l'arbre coherent.
This commit is contained in:
2026-09-21 06:38:02 -04:00
parent 95bc861cdb
commit ce0d561ade
13 changed files with 774 additions and 85 deletions
+29 -4
View File
@@ -94,7 +94,27 @@
<div class="sd-user-add">Invite</div>
</div>
</template>
<div class="sd-user-empty" x-show="!inviteUsers.length">No matching users</div>
<template x-if="inviteGroups && inviteGroups.length">
<div>
<div class="sd-user-empty" style="font-weight:600;">Groups</div>
<template x-for="g in inviteGroups" :key="'g'+g.id">
<div
class="sd-user-item"
@click="pickInviteGroup(g)"
>
<div class="sd-user-avatar" style="background:#5b5bd6;color:#fff;">
<span x-text="(g.name || '?').charAt(0).toUpperCase()"></span>
</div>
<div class="sd-user-meta">
<div class="sd-user-login" x-text="g.name"></div>
<div class="sd-user-name" x-text="'Group · ' + (g.member_count || 0) + ' members'"></div>
</div>
<div class="sd-user-add">Invite</div>
</div>
</template>
</div>
</template>
<div class="sd-user-empty" x-show="!inviteUsers.length && !(inviteGroups && inviteGroups.length)">No matching users</div>
</div>
</div>
@@ -113,17 +133,22 @@
<div class="sd-avatar" x-show="a.user_avatar_url" style="overflow:hidden">
<img :src="a.user_avatar_url" style="width:100%;height:100%;border-radius:50%;object-fit:cover;" />
</div>
<div class="sd-avatar" x-show="!a.user_avatar_url"
<!-- Group avatar -->
<div class="sd-avatar" x-show="(a.kind === 'group' || a.shared_with_group_id) && !a.user_avatar_url"
style="background:#5b5bd6;color:#fff;"
x-text="(a.group_name || '?')[0].toUpperCase()"
></div>
<div class="sd-avatar" x-show="!(a.kind === 'group' || a.shared_with_group_id) && !a.user_avatar_url"
x-text="(a.user_full_name || a.user_login || a.shared_with_email || '?')[0].toUpperCase()"
></div>
<div>
<div
class="sd-participant-name"
x-text="a.user_full_name || a.user_login || a.shared_with_email"
x-text="(a.kind === 'group' || a.shared_with_group_id) ? (a.group_name || 'Group') : (a.user_full_name || a.user_login || a.shared_with_email)"
></div>
<div
class="sd-participant-email"
x-text="a.shared_with_email || a.user_login || ''"
x-text="(a.kind === 'group' || a.shared_with_group_id) ? 'Group' : (a.shared_with_email || a.user_login || '')"
></div>
</div>
</div>