From fa38dbe06d6ddb5f4499b759459a24d2b3c111e8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 7 Jul 2023 13:36:21 -0400
Subject: a bunch of fixes to simplify collaboration and make it work better.

---
 src/client/util/SharingManager.tsx | 203 ++++++++-----------------------------
 1 file changed, 41 insertions(+), 162 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 64b2bb5b8..d3241009a 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -6,10 +6,9 @@ import * as React from 'react';
 import Select from 'react-select';
 import * as RequestPromise from 'request-promise';
 import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
-import { AclAdmin, AclPrivate, AclUnset, DocAcl, DocData } from '../../fields/DocSymbols';
+import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
-import { List } from '../../fields/List';
-import { DocCast, NumCast, StrCast } from '../../fields/Types';
+import { StrCast } from '../../fields/Types';
 import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
 import { Utils } from '../../Utils';
 import { DocServer } from '../DocServer';
@@ -73,13 +72,11 @@ export class SharingManager extends React.Component<{}> {
     @observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals
     @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups
     private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup
-    private distributeAclsButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the distribute button, used for the position of the popup
     // if both showUserOptions and showGroupOptions are false then both are displayed
     @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component)
     @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component)
     private populating: boolean = false; // whether the list of users is populating or not
     @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
-    @observable private overrideNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private
     @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not
 
     // private get linkVisible() {
@@ -110,7 +107,6 @@ export class SharingManager extends React.Component<{}> {
             500
         );
         this.layoutDocAcls = false;
-        this.overrideNested = false;
     });
 
     constructor(props: {}) {
@@ -153,41 +149,19 @@ export class SharingManager extends React.Component<{}> {
     /**
      * Shares the document with a user.
      */
-    setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => {
+    setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => {
         const { user, sharingDoc } = recipient;
         const target = targetDoc || this.targetDoc!;
         const acl = `acl-${normalizeEmail(user.email)}`;
         const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`;
-        const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1;
-
-        // setting the same acl for a docs within the doc being shared if they haven't been set yet
-        // or if the 'Override Nested' checkbox is selected
-        var childDocs = DocListCast(target.data);
-        childDocs.map(doc => {
-            if (this.overrideNested || doc[acl] == undefined) {
-                this.setInternalSharing(recipient, permission, doc);
-            }
-        });
-
-        const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
-
-        // ! ensures it returns true if document has been shared successfully, false otherwise
-        return !docs
-            .map(doc => (this.layoutDocAcls ? doc : Doc.GetProto(doc)))
-            .map(doc => {
-                doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
+        const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc);
+        docs.map(doc => (this.layoutDocAcls ? doc : Doc.GetProto(doc))).forEach(doc => {
+            doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined);
 
-                if (permission === SharingPermissions.None) {
-                    if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1;
-                } else {
-                    if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1;
-                }
-                distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
-                this.setDashboardBackground(doc, permission as SharingPermissions);
-                if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc);
-                return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc);
-            })
-            .some(success => !success);
+            distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined);
+            if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc);
+            else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc);
+        });
     };
 
     /**
@@ -199,51 +173,24 @@ export class SharingManager extends React.Component<{}> {
         const target = targetDoc || this.targetDoc!;
         const key = normalizeEmail(StrCast(group.title));
         let acl = `acl-${key}`;
-        const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1;
-
-        // setting the same acl for a docs within the doc being shared if they haven't been set yet
-        // or if the 'Override Private' checkbox is selected
-        var childDocs = DocListCast(target.data);
-        childDocs.map(doc => {
-            if (this.overrideNested || doc[acl] == undefined) {
-                this.setInternalGroupSharing(group, permission, doc);
-            }
-        });
-
-        const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
-
-        if (acl == 'acl-Public' && this.layoutDocAcls) acl = 'acl-Public-layout';
-        // ! ensures it returns true if document has been shared successfully, false otherwise
-        return !docs
-            .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
-            .map(doc => {
-                doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
-
-                if (permission === SharingPermissions.None) {
-                    if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1;
-                } else {
-                    if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1;
-                }
 
-                distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
-                this.setDashboardBackground(doc, permission as SharingPermissions);
+        const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc);
+        docs.map(doc => (this.layoutDocAcls ? doc : Doc.GetProto(doc))).forEach(doc => {
+            doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined);
 
-                if (group instanceof Doc) {
-                    const members: string[] = JSON.parse(StrCast(group.members));
-                    const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
+            distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined);
 
-                    // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc
-                    group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : (group.docsShared = new List<Doc>([doc]));
+            if (group instanceof Doc) {
+                Doc.AddDocToList(group, 'docsShared', doc);
 
-                    return users
-                        .map(({ user, sharingDoc }) => {
-                            if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
-                            else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists
-                        })
-                        .some(success => !success);
-                }
-            })
-            .some(success => success);
+                this.users
+                    .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email))
+                    .forEach(({ user, sharingDoc }) => {
+                        if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
+                        else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists
+                    });
+            }
+        });
     };
 
     /**
@@ -279,44 +226,15 @@ export class SharingManager extends React.Component<{}> {
                 else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc);
             });
         } else {
-            const dashboards = DocListCast(Doc.MyDashboards.data);
             docs.forEach(doc => {
-                const isDashboard = dashboards.indexOf(doc) !== -1;
-                if (this.overrideNested) {
-                    this.shareFromPropertiesSidebar(shareWith, permission, DocListCast(doc.data), layout);
-                }
                 if (GetEffectiveAcl(doc) === AclAdmin) {
-                    if (shareWith == 'Public' && layout) shareWith = 'Public-layout';
-                    distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard);
+                    distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined);
                 }
-                this.setDashboardBackground(doc, permission as SharingPermissions);
             });
         }
         this.layoutDocAcls = false;
     };
 
-    /**
-     * Sets the background of the Dashboard if it has been shared as a visual indicator
-     */
-    setDashboardBackground = (doc: Doc, permission: SharingPermissions) => {
-        if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) {
-            if (permission !== SharingPermissions.None) {
-                doc.isShared = true;
-                doc.backgroundColor = 'green';
-            } else {
-                const acls = doc[DocData][DocAcl];
-                if (
-                    Object.keys(acls)
-                        .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && key !== 'acl-Me')
-                        .every(key => [AclUnset, AclPrivate].includes(acls[key]))
-                ) {
-                    doc.isShared = undefined;
-                    doc.backgroundColor = undefined;
-                }
-            }
-        }
-    };
-
     /**
      * Removes the documents shared with a user through a group when the user is removed from the group.
      * @param group
@@ -341,12 +259,9 @@ export class SharingManager extends React.Component<{}> {
      */
     removeGroup = (group: Doc) => {
         if (group.docsShared) {
-            const dashboards = DocListCast(Doc.MyDashboards.data);
             DocListCast(group.docsShared).forEach(doc => {
                 const acl = `acl-${StrCast(group.title)}`;
-                const isDashboard = dashboards.indexOf(doc) !== -1;
-                distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined, isDashboard);
-                distributeAcls(acl, SharingPermissions.None, Doc.GetProto(doc), undefined, undefined, isDashboard);
+                distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined);
 
                 const members: string[] = JSON.parse(StrCast(group.members));
                 const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
@@ -440,7 +355,7 @@ export class SharingManager extends React.Component<{}> {
         if (this.selectedUsers) {
             this.selectedUsers.forEach(user => {
                 if (user.value.includes(indType)) {
-                    this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions);
+                    this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined);
                 } else {
                     this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
                 }
@@ -457,7 +372,6 @@ export class SharingManager extends React.Component<{}> {
             );
 
             this.layoutDocAcls = false;
-            this.overrideNested = false;
             this.selectedUsers = null;
         }
     };
@@ -509,8 +423,7 @@ export class SharingManager extends React.Component<{}> {
         const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users;
         const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
 
-        // handles the case where multiple documents are selected
-        let docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DocData]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DocData]));
+        let docs = SelectionManager.Views().length < 2 ? [this.targetDoc] : SelectionManager.Views().map(docView => docView.rootDoc);
 
         if (this.myDocAcls) {
             const newDocs: Doc[] = [];
@@ -518,36 +431,32 @@ export class SharingManager extends React.Component<{}> {
             docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
         }
 
-        const targetDoc: Doc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
+        const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
 
         // tslint:disable-next-line: no-unnecessary-callback-wrapper
         const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc));
         const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin);
 
         // users in common between all docs
-        const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl])));
+        const commonKeys = intersection(docs).reduce((list, doc) => (doc?.[DocAcl] ? [...list, ...Object.keys(doc[DocAcl])] : list), [] as string[]);
 
         // the list of users shared with
-        const userListContents: (JSX.Element | null)[] = users
+        const userListContents = users
             // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
             .filter(({ user }) => docs[0]?.author !== user.email)
             .map(({ user, linkDatabase, sharingDoc, userColor }) => {
                 const userKey = `acl-${normalizeEmail(user.email)}`;
-                const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
+                const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
                 // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
-                let permissions = this.layoutDocAcls ? (targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(Doc.GetProto(targetDoc)[userKey])) : StrCast(targetDoc[userKey]);
-                if (this.layoutDocAcls) {
-                    if (targetDoc[DocAcl][userKey]) permissions = HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name;
-                    else if (targetDoc['embedContainer']) permissions = StrCast(Doc.GetProto(DocCast(targetDoc['embedContainer']))[userKey]);
-                    else permissions = uniform ? StrCast(Doc.GetProto(targetDoc)?.[userKey]) : '-multiple-';
-                } else permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
+                let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]);
+                permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
 
                 return !permissions ? null : (
                     <div key={userKey} className={'container'}>
                         <span className={'padding'}>{user.email}</span>
                         <div className="edit-actions">
                             {admin || this.myDocAcls ? (
-                                <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}>
+                                <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}>
                                     {this.sharingOptions(uniform)}
                                 </select>
                             ) : (
@@ -566,12 +475,7 @@ export class SharingManager extends React.Component<{}> {
 
         // the owner of the doc and the current user are placed at the top of the user list.
         const userKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`;
-        var curUserPermission;
-        if (this.layoutDocAcls) {
-            if (targetDoc[DocAcl][userKey]) curUserPermission = HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name;
-            else if (targetDoc['embedContainer']) curUserPermission = StrCast(Doc.GetProto(DocCast(targetDoc['embedContainer']))[userKey]);
-            else curUserPermission = StrCast(Doc.GetProto(targetDoc)?.[userKey]);
-        } else curUserPermission = StrCast(targetDoc[userKey]);
+        const curUserPermission = StrCast(targetDoc[userKey]);
         // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name
         userListContents.unshift(
             sameAuthor ? (
@@ -600,20 +504,8 @@ export class SharingManager extends React.Component<{}> {
         groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" });
         const groupListContents = groupListMap.map(group => {
             let groupKey = `acl-${StrCast(group.title)}`;
-            const uniform = docs
-                .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
-                .every(doc => (this.layoutDocAcls ? doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey] : doc?.[DocData]?.[DocAcl]?.[groupKey] === docs[0]?.[DocData]?.[DocAcl]?.[groupKey]));
-            // const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-';
-            let permissions = this.layoutDocAcls ? (targetDoc[DocAcl][groupKey] ? HierarchyMapping.get(targetDoc[DocAcl][groupKey])?.name : StrCast(Doc.GetProto(targetDoc)[groupKey])) : StrCast(targetDoc[groupKey]);
-            if (this.layoutDocAcls) {
-                if (groupKey == 'acl-Public') groupKey = 'acl-Public-layout';
-                if (targetDoc[DocAcl][groupKey]) permissions = HierarchyMapping.get(targetDoc[DocAcl][groupKey])?.name;
-                else {
-                    if (groupKey == 'acl-Public-layout') groupKey = 'acl-Public';
-                    if (targetDoc['embedContainer']) permissions = StrCast(Doc.GetProto(DocCast(targetDoc['embedContainer']))[groupKey]);
-                    else permissions = uniform ? StrCast(Doc.GetProto(targetDoc)?.[groupKey]) : '-multiple-';
-                }
-            } else permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-';
+            const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]);
+            const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-';
 
             return !permissions ? null : (
                 <div key={groupKey} className={'container'}>
@@ -693,7 +585,6 @@ export class SharingManager extends React.Component<{}> {
                             <div className="acl-container">
                                 {Doc.noviceMode ? null : (
                                     <div className="layoutDoc-acls">
-                                        <input type="checkbox" onChange={action(() => (this.overrideNested = !this.overrideNested))} checked={this.overrideNested} /> <label>Override Nested </label>
                                         <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label>
                                     </div>
                                 )}
@@ -713,16 +604,10 @@ export class SharingManager extends React.Component<{}> {
                             <div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}>
                                 <div className="title-individual">
                                     Individuals &nbsp;
-                                    {this.individualSort === 'ascending' ? (
-                                        <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
-                                    ) : this.individualSort === 'descending' ? (
-                                        <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
-                                    ) : (
-                                        <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
-                                    )}
+                                    <FontAwesomeIcon icon={this.individualSort === 'ascending' ? 'caret-up' : this.individualSort === 'descending' ? 'caret-down' : 'caret-right'} size="xs" />
                                 </div>
                             </div>
-                            <div className={'users-list'}>{userListContents}</div>
+                            <div className="users-list">{userListContents}</div>
                         </div>
                         <div className={'group-container'}>
                             <div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
@@ -732,13 +617,7 @@ export class SharingManager extends React.Component<{}> {
                                         <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} />
                                     </div>
                                     &nbsp;
-                                    {this.groupSort === 'ascending' ? (
-                                        <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
-                                    ) : this.groupSort === 'descending' ? (
-                                        <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
-                                    ) : (
-                                        <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
-                                    )}
+                                    <FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} size="xs" />
                                 </div>
                             </div>
                             <div className={'groups-list'}>{groupListContents}</div>
-- 
cgit v1.2.3-70-g09d2

