feat(share,permissions): partage de page par groupes (page_shares)
- 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:
@@ -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>
|
||||
|
||||
@@ -1023,7 +1023,7 @@ window.__fdEditorScriptsLoaded = true;
|
||||
generalAccess:'{{ page_share_mode }}',publicPerm:'viewer',
|
||||
pubAllowEdit:false,pubAllowComments:true,
|
||||
pageUrl:window.location.href,publishedUrl:'',
|
||||
inviteEmail:'',invitePermission:'edit',inviteUserId:null,inviteUsers:[],inviteSuggestOpen:false,inviteSel:-1,accessList:[],
|
||||
inviteEmail:'',invitePermission:'edit',inviteUserId:null,inviteGroupId:null,inviteGroups:[],inviteUsers:[],inviteSuggestOpen:false,inviteSel:-1,accessList:[],
|
||||
toastVisible:false,toastMsg:'',
|
||||
// ── v4.9.0: Collaboration — comments & mentions ──
|
||||
commentsOpen:false,commentDraft:'',comments:[],commentCount:0,
|
||||
@@ -1659,15 +1659,30 @@ applyAIBlocks(text){
|
||||
},
|
||||
shareInvite() {
|
||||
var self = this;
|
||||
if (this.inviteGroupId) {
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/share', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
|
||||
body: JSON.stringify({ group_id: this.inviteGroupId, permission: this.invitePermission })
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.status === 'shared') {
|
||||
self.inviteEmail = ''; self.inviteUserId = null; self.inviteGroupId = null;
|
||||
self.inviteUsers = []; self.inviteGroups = []; self.inviteSuggestOpen = false;
|
||||
self.showToast('Group shared'); self.loadShares();
|
||||
} else { self.showToast(d.detail || 'Share failed'); }
|
||||
}).catch(function(){ self.showToast('Share failed'); });
|
||||
return;
|
||||
}
|
||||
if (!this.inviteEmail.trim()) return;
|
||||
var csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
var csrf2 = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch('/api/pages/' + this.pid + '/share', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf ? csrf[1] : '' },
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf2 ? csrf2[1] : '' },
|
||||
body: JSON.stringify({ user_id: this.inviteUserId, email: this.inviteEmail.trim(), permission: this.invitePermission })
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if (d.status === 'shared') {
|
||||
self.inviteEmail = ''; self.inviteUserId = null; self.inviteUsers = []; self.inviteSuggestOpen = false;
|
||||
self.inviteEmail = ''; self.inviteUserId = null; self.inviteUsers = []; self.inviteGroups = []; self.inviteSuggestOpen = false;
|
||||
self.showToast('Invitation sent'); self.loadShares();
|
||||
} else { self.showToast(d.detail || 'Share failed'); }
|
||||
}).catch(function(){ self.showToast('Share failed'); });
|
||||
@@ -1675,7 +1690,7 @@ applyAIBlocks(text){
|
||||
inviteInput(){
|
||||
var self = this;
|
||||
var q = (this.inviteEmail || '').trim();
|
||||
if (!q) { this.inviteUsers = []; this.inviteSuggestOpen = false; this.inviteUserId = null; this.inviteSel = -1; return; }
|
||||
if (!q) { this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteUserId = null; this.inviteGroupId = null; this.inviteSel = -1; return; }
|
||||
clearTimeout(this._inviteTimer);
|
||||
this._inviteTimer = setTimeout(function(){ self._loadInviteUsers(q); }, 150);
|
||||
},
|
||||
@@ -1689,9 +1704,20 @@ applyAIBlocks(text){
|
||||
this.inviteUsers = (d.users || []).filter(function(u){
|
||||
return u.id !== me && !shared[(u.login || '').toLowerCase()];
|
||||
});
|
||||
// Groups: charger les groupes du workspace et filtrer
|
||||
try {
|
||||
const gr = await fetch('/api/v2/groups', { credentials: 'same-origin' });
|
||||
const gd = await gr.json();
|
||||
var sharedGroups = {};
|
||||
(this.accessList || []).forEach(function(a){ if (a.shared_with_group_id) sharedGroups[a.shared_with_group_id] = true; });
|
||||
var ql = (q || '').toLowerCase();
|
||||
this.inviteGroups = (gd.groups || []).filter(function(g){
|
||||
return !sharedGroups[g.id] && (!ql || (g.name || '').toLowerCase().indexOf(ql) >= 0);
|
||||
}).slice(0, 8);
|
||||
} catch(e2) { this.inviteGroups = []; }
|
||||
this.inviteSel = this.inviteUsers.length ? 0 : -1;
|
||||
this.inviteSuggestOpen = true;
|
||||
} catch(e) { this.inviteUsers = []; this.inviteSuggestOpen = false; }
|
||||
} catch(e) { this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; }
|
||||
},
|
||||
inviteMove(dir){
|
||||
var el = this.$refs.inviteSuggest;
|
||||
@@ -1713,8 +1739,17 @@ applyAIBlocks(text){
|
||||
},
|
||||
pickInviteUser(u){
|
||||
this.inviteUserId = u.id;
|
||||
this.inviteGroupId = null;
|
||||
this.inviteEmail = u.login || u.full_name || '';
|
||||
this.inviteUsers = []; this.inviteSuggestOpen = false; this.inviteSel = -1;
|
||||
this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteSel = -1;
|
||||
var el = this.$refs.inviteInput;
|
||||
if (el) el.focus();
|
||||
},
|
||||
pickInviteGroup(g){
|
||||
this.inviteGroupId = g.id;
|
||||
this.inviteUserId = null;
|
||||
this.inviteEmail = g.name || '';
|
||||
this.inviteUsers = []; this.inviteGroups = []; this.inviteSuggestOpen = false; this.inviteSel = -1;
|
||||
var el = this.$refs.inviteInput;
|
||||
if (el) el.focus();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user