diff options
-rw-r--r-- | src/client/DocServer.ts | 14 | ||||
-rw-r--r-- | src/client/util/SharingManager.tsx | 205 | ||||
-rw-r--r-- | src/client/views/DashboardView.scss | 141 | ||||
-rw-r--r-- | src/client/views/DashboardView.tsx | 32 | ||||
-rw-r--r-- | src/client/views/DocComponent.tsx | 61 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 33 | ||||
-rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 12 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/PropertiesView.tsx | 50 | ||||
-rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 17 | ||||
-rw-r--r-- | src/client/views/collections/TreeView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 2 | ||||
-rw-r--r-- | src/fields/Doc.ts | 4 | ||||
-rw-r--r-- | src/fields/util.ts | 125 |
14 files changed, 265 insertions, 441 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 2a7f5a09b..8b8a9a618 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -76,10 +76,14 @@ export namespace DocServer { const fieldWriteModes: { [field: string]: WriteMode } = {}; const docsWithUpdates: { [field: string]: Set<Doc> } = {}; - export var PlaygroundFields: string[]; - export function setPlaygroundFields(livePlaygroundFields: string[]) { - DocServer.PlaygroundFields = livePlaygroundFields; - livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground)); + export var PlaygroundFields: string[] = []; + export function setLivePlaygroundFields(livePlaygroundFields: string[]) { + DocServer.PlaygroundFields.push(...livePlaygroundFields); + livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground)); + } + export function setPlaygroundFields(playgroundFields: string[]) { + DocServer.PlaygroundFields.push(...playgroundFields); + playgroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground)); } export function IsPlaygroundField(field: string) { return DocServer.PlaygroundFields?.includes(field.replace(/^_/, '')); @@ -97,7 +101,7 @@ export namespace DocServer { } export function getFieldWriteMode(field: string) { - return Doc.CurrentUserEmail === 'guest' ? WriteMode.LiveReadonly : fieldWriteModes[field] || WriteMode.Default; + return Doc.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default; } export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) { diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 3a0672113..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 - initially selected so default is override @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not // private get linkVisible() { @@ -94,7 +91,6 @@ export class SharingManager extends React.Component<{}> { DictationOverlay.Instance.hasActiveModal = true; this.isOpen = this.targetDoc !== undefined; this.permissions = SharingPermissions.Augment; - this.overrideNested = true; }); }; @@ -111,7 +107,6 @@ export class SharingManager extends React.Component<{}> { 500 ); this.layoutDocAcls = false; - this.overrideNested = false; }); constructor(props: {}) { @@ -154,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); + }); }; /** @@ -200,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 Nested' 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 + }); + } + }); }; /** @@ -280,45 +226,16 @@ 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 * @param emailId @@ -342,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)); @@ -441,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); } @@ -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 ? ( @@ -582,7 +486,7 @@ export class SharingManager extends React.Component<{}> { </div> </div> ) : null, - sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail && Doc.CurrentUserEmail != 'guest' ? ( + sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? ( <div key={'me'} className={'container'}> <span className={'padding'}>Me</span> <div className="edit-actions"> @@ -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 - {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> - {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> diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss index b8a6f6c05..583edac08 100644 --- a/src/client/views/DashboardView.scss +++ b/src/client/views/DashboardView.scss @@ -1,39 +1,40 @@ -@import "./global/globalCssVariables"; - +@import './global/globalCssVariables'; .dashboard-view { - padding: 50px; - display: flex; - flex-direction: row; - width: 100%; - position: absolute; - - .left-menu { - display: flex; - justify-content: flex-start; - flex-direction: column; - width: 250px; - min-width: 250px; - } - - .all-dashboards { - display: flex; - flex-direction: row; - flex-wrap: wrap; - overflow-y: scroll; - } + padding: 50px; + display: flex; + flex-direction: row; + width: 100%; + position: absolute; + height: 100%; + overflow: auto; + + .left-menu { + display: flex; + justify-content: flex-start; + flex-direction: column; + width: 250px; + min-width: 250px; + } + + .all-dashboards { + display: flex; + flex-direction: row; + flex-wrap: wrap; + overflow-y: scroll; + } } .text-button { cursor: pointer; - padding: 3px 0; - &:hover { - font-weight: 500; - } - - &.selected { - font-weight: 700; - } + padding: 3px 0; + &:hover { + font-weight: 500; + } + + &.selected { + font-weight: 700; + } } .new-dashboard-button { @@ -64,41 +65,51 @@ } .dashboard-container { - border-radius: 10px; - cursor: pointer; - width: 250px; - height: 200px; - outline: solid 2px $light-gray; - display: flex; - flex-direction: column; - margin: 0 0px 30px 30px; - overflow: hidden; - - &:hover{ + border-radius: 10px; + cursor: pointer; + width: 250px; + height: 200px; + outline: solid 2px $light-gray; + display: flex; + flex-direction: column; + margin: 0 0px 30px 30px; + overflow: hidden; + + &:hover { outline: solid 2px $light-blue; - } - - .title { - margin: 10px; - font-weight: 500; - } - - img { - width: auto; - height: 80%; - } - - .info { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 0px 10px; - } - - .more { - z-index: 100; - } + } + + .title { + margin: 10px; + font-weight: 500; + } + + img { + width: auto; + height: 80%; + } + + .info { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0px 10px; + } + .dashboard-status, + .dashboard-status-shared { + font-size: 9; + left: 10%; + position: relative; + top: -5; + } + .dashboard-status-shared { + background: 'lightgreen'; + } + + .more { + z-index: 100; + } } .new-dashboard { @@ -136,4 +147,4 @@ flex-direction: row; justify-content: flex-end; } -}
\ No newline at end of file +} diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index b60b84015..123090fcf 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -4,7 +4,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, DocListCastAsync } from '../../fields/Doc'; -import { DocData } from '../../fields/DocSymbols'; +import { AclPrivate, AclUnset, DocAcl, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; @@ -62,15 +62,13 @@ export class DashboardView extends React.Component { Doc.ActivePage = 'dashboard'; }; - getDashboards = () => { + getDashboards = (whichGroup: DashboardGroup) => { const allDashboards = DocListCast(Doc.MyDashboards.data); - if (this.selectedDashboardGroup === DashboardGroup.MyDashboards) { + if (whichGroup === DashboardGroup.MyDashboards) { return allDashboards.filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail); - } else { - const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc.dockingConfig) - // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking); - return sharedDashboards; } + const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc.dockingConfig); + return sharedDashboards; }; isUnviewedSharedDashboard = (dashboard: Doc): boolean => { @@ -159,24 +157,33 @@ export class DashboardView extends React.Component { <Button icon={<FaPlus />} size={Size.MEDIUM} text="New" onClick={() => this.setNewDashboardName('')} /> </div> <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)}> - My Dashboards + {'My Dashboards (' + this.getDashboards(DashboardGroup.MyDashboards).length + ')'} </div> <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.SharedDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)}> - Shared Dashboards + {'Shared Dashboards (' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'} </div> </div> <div className="all-dashboards"> - {this.getDashboards().map(dashboard => { + {this.getDashboards(this.selectedDashboardGroup).map(dashboard => { const href = ImageCast(dashboard.thumb)?.url.href; + const shared = Object.keys(dashboard[DocAcl]) + .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && !['acl-Me', 'acl-Public'].includes(key)) + .some(key => ![AclUnset, AclPrivate].includes(dashboard[DocAcl][key])); return ( - <div className="dashboard-container" key={dashboard[Id]} onContextMenu={e => this.onContextMenu(dashboard, e)} onClick={e => this.clickDashboard(e, dashboard)}> + <div className="dashboard-container" key={dashboard[Id]} style={{ background: shared ? 'lightgreen' : '' }} onContextMenu={e => this.onContextMenu(dashboard, e)} onClick={e => this.clickDashboard(e, dashboard)}> <img src={ href ?? 'https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU=' } /> <div className="info"> - <input style={{ border: 'unset' }} className="input" onClick={e => e.stopPropagation()} defaultValue={StrCast(dashboard.title)} onChange={e => (Doc.GetProto(dashboard).title = (e.target as any).value)} /> + <input + style={{ border: 'unset', borderRadius: '5px' }} + className="input" + onClick={e => e.stopPropagation()} + defaultValue={StrCast(dashboard.title)} + onChange={e => (Doc.GetProto(dashboard).title = (e.target as any).value)} + /> {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>} <div className="more" @@ -192,6 +199,7 @@ export class DashboardView extends React.Component { <Button size={Size.SMALL} icon={<FontAwesomeIcon color="black" size="lg" icon="bars" />} /> </div> </div> + <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div> </div> ); })} diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 1d0feec74..44af51341 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,18 +1,16 @@ import { action, computed, observable } from 'mobx'; import { DateField } from '../../fields/DateField'; -import { DocListCast, Opt, Doc, ReverseHierarchyMap, HierarchyMapping } from '../../fields/Doc'; +import { Doc, DocListCast, HierarchyMapping, Opt, ReverseHierarchyMap } from '../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocAcl, DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; -import { Cast, DocCast, ScriptCast, StrCast } from '../../fields/Types'; -import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, normalizeEmail, SharingPermissions } from '../../fields/util'; +import { Cast, StrCast } from '../../fields/Types'; +import { distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; import { returnFalse } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { InteractionUtils } from '../util/InteractionUtils'; -import { UndoManager } from '../util/UndoManager'; import { DocumentView } from './nodes/DocumentView'; import { Touchable } from './Touchable'; -import { SharingManager } from '../util/SharingManager'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) export interface DocComponentProps { @@ -184,7 +182,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() if (this.props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) { return false; } - const targetDataDoc = this.props.Document[DocData]; + const targetDataDoc = this.rootDoc[DocData]; const effectiveAcl = GetEffectiveAcl(targetDataDoc); if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { @@ -194,52 +192,33 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() if (added.length) { const aclKeys = Object.keys(Doc.GetProto(this.props.Document)[DocAcl] ?? {}); - aclKeys.forEach(key => - added.forEach(d => { - if (key != 'acl-Me') { - const permissionString = StrCast(Doc.GetProto(this.props.Document)[key]); - const permissionSymbol = ReverseHierarchyMap.get(permissionString)!.acl; - const permission = HierarchyMapping.get(permissionSymbol)!.name; - distributeAcls(key, permission, Doc.GetProto(d)); - } - }) - ); + GetEffectiveAcl(this.rootDoc) === AclAdmin && + aclKeys.forEach(key => + added.forEach(d => { + if (key != 'acl-Me') { + const permissionString = StrCast(Doc.GetProto(this.props.Document)[key]); + const permissionSymbol = ReverseHierarchyMap.get(permissionString)?.acl; + const permission = permissionSymbol && HierarchyMapping.get(permissionSymbol)?.name; + distributeAcls(key, permission ?? SharingPermissions.Augment, Doc.GetProto(d)); + } + }) + ); if (effectiveAcl === AclAugment) { added.map(doc => { - Doc.SetContainer(doc, this.props.Document); - if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document; + if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc); - const parent = DocCast(doc.embedContainer); - doc.embedContainer && inheritParentAcls(parent, doc); - for (const key of Object.keys(parent)) { - const symbol = ReverseHierarchyMap.get(StrCast(parent[key])); - if (symbol && key.startsWith('acl')) { - const sharePermission = HierarchyMapping.get(symbol.acl!)!.name; - const user = SharingManager.Instance?.users.filter(({ user: { email } }) => normalizeEmail(email) == key.slice(4))[0]; - if (user && sharePermission !== SharingPermissions.None) return Doc.AddDocToList(user.sharingDoc, 'data', doc); - } - } + Doc.SetContainer(doc, targetDataDoc); + inheritParentAcls(targetDataDoc, doc); }); } else { added .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) .map(doc => { - // only make a pushpin if we have acl's to edit the document - //DocUtils.LeavePushpin(doc); doc._dragOnlyWithinContainer = undefined; - Doc.SetContainer(doc, this.props.Document); if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; - const parent = DocCast(doc.embedContainer); - doc.embedContainer && inheritParentAcls(parent, doc); - for (const key of Object.keys(Doc.GetProto(parent))) { - const symbol = ReverseHierarchyMap.get(StrCast(parent[key])); - if (symbol && key.startsWith('acl')) { - const sharePermission = HierarchyMapping.get(symbol.acl!)!.name; - const user = SharingManager.Instance?.users.filter(({ user: { email } }) => normalizeEmail(email) == key.slice(4))[0]; - if (user && sharePermission !== SharingPermissions.None) return Doc.AddDocToList(user.sharingDoc, 'data', doc); - } - } + Doc.SetContainer(doc, this.rootDoc); + inheritParentAcls(targetDataDoc, doc); }); const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 4cd172034..3522830e5 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -5,7 +5,6 @@ import { IconButton } from 'browndash-components'; import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { FaUndo } from 'react-icons/fa'; -import { Utils, aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents } from '../../Utils'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Field, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData, Height, Width } from '../../fields/DocSymbols'; @@ -13,27 +12,27 @@ import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; -import { GetEffectiveAcl, GetEffectiveLayoutAcl, normalizeEmail, SharingPermissions } from '../../fields/util'; -import { DocumentType } from '../documents/DocumentTypes'; +import { GetEffectiveAcl } from '../../fields/util'; +import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; +import { DocumentType } from '../documents/DocumentTypes'; import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; -import { SettingsManager } from '../util/SettingsManager'; import { SnappingManager } from '../util/SnappingManager'; import { UndoManager } from '../util/UndoManager'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; -import { InkStrokeProperties } from './InkStrokeProperties'; +import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; +import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; -import { CollectionDockingView } from './collections/CollectionDockingView'; -import { CollectionFreeFormView } from './collections/collectionFreeForm'; -import { Colors } from './global/globalEnums'; import { DocumentView, OpenWhereMod } from './nodes/DocumentView'; -import { ImageBox } from './nodes/ImageBox'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { ImageBox } from './nodes/ImageBox'; import React = require('react'); import _ = require('lodash'); @@ -165,7 +164,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onContainerDown = (e: React.PointerEvent): void => { const first = SelectionManager.Views()[0]; - const effectiveLayoutAcl = GetEffectiveLayoutAcl(first.rootDoc); + const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) { setupMoveUpEvents( this, @@ -179,7 +178,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onTitleDown = (e: React.PointerEvent): void => { const first = SelectionManager.Views()[0]; - const effectiveLayoutAcl = GetEffectiveLayoutAcl(first.rootDoc); + const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) { setupMoveUpEvents( this, @@ -200,7 +199,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { const first = SelectionManager.Views()[0]; - const effectiveLayoutAcl = GetEffectiveLayoutAcl(first.rootDoc); + const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); if (effectiveLayoutAcl != AclAdmin && effectiveLayoutAcl != AclEdit && effectiveLayoutAcl != AclAugment) { return false; } @@ -496,7 +495,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { const first = SelectionManager.Views()[0]; - const effectiveAcl = GetEffectiveLayoutAcl(first.rootDoc); + const effectiveAcl = GetEffectiveAcl(first.rootDoc); if (!(effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment)) return false; if (!first) return false; let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; @@ -765,7 +764,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } // sharing - const acl = this.showLayoutAcl ? GetEffectiveLayoutAcl(seldocview.rootDoc) : GetEffectiveAcl(seldocview.rootDoc); + const acl = GetEffectiveAcl(!this.showLayoutAcl ? Doc.GetProto(seldocview.rootDoc) : seldocview.rootDoc); const docShareMode = HierarchyMapping.get(acl)!.name; const shareMode = StrCast(docShareMode); var shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image; @@ -773,7 +772,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P // hide the decorations if the parent chooses to hide it or if the document itself hides it const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations; const hideResizers = - ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveLayoutAcl(seldocview.rootDoc)) || hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating; + ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(seldocview.rootDoc)) || hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating; const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.layout_hideDecorationTitle || this._isRounding || this._isRotating; const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.layout_hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button @@ -832,14 +831,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P {shareSymbolIcon + ' ' + shareMode} - {/* {!Doc.noviceMode ? ( + {!Doc.noviceMode ? ( <div className="checkbox"> <div className="checkbox-box"> <input type="checkbox" checked={this.showLayoutAcl} onChange={action(() => (this.showLayoutAcl = !this.showLayoutAcl))} /> </div> <div className="checkbox-text"> Layout </div> </div> - ) : null} */} + ) : null} </div> </div> diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 47dcdd2e4..347c40c18 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -285,13 +285,17 @@ export class KeyManager { preventDefault = false; break; case 'y': - SelectionManager.DeselectAll(); - UndoManager.Redo(); + if (Doc.ActivePage !== 'home') { + SelectionManager.DeselectAll(); + UndoManager.Redo(); + } stopPropagation = false; break; case 'z': - SelectionManager.DeselectAll(); - UndoManager.Undo(); + if (Doc.ActivePage !== 'home') { + SelectionManager.DeselectAll(); + UndoManager.Undo(); + } stopPropagation = false; break; case 'a': diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 258674d53..b708e2587 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -153,10 +153,12 @@ export class MainView extends React.Component { } this._sidebarContent.proto = undefined; if (!MainView.Live) { - DocServer.setPlaygroundFields([ + DocServer.setPlaygroundFields(['dockingConfig']); + DocServer.setLivePlaygroundFields([ 'dataTransition', 'viewTransition', 'treeViewOpen', + 'treeViewExpandedView', 'carousel_index', 'itemIndex', // for changing slides in presentations 'layout_sidebarWidthPercent', diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 2b12a7b58..633401d58 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -395,9 +395,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div> <div className={'propertiesView-shareDropDown'}> <div className={`propertiesView-shareDropDown${permission}`}> - <div className="propertiesView-shareDropDown"> - {admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission) : concat(shareImage, ' ', permission)} - </div> + <div className="propertiesView-shareDropDown">{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission) : concat(shareImage, ' ', permission)}</div> </div> </div> </div> @@ -425,11 +423,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @computed get sharingTable() { // all selected docs - const docs = - SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutDocAcls ? this.selectedDoc : this.dataDoc!] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DocData])); + const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.selectedDoc] : SelectionManager.Views().map(docView => docView.rootDoc); const target = docs[0]; - const showAdmin = GetEffectiveAcl(target) == AclAdmin + const showAdmin = GetEffectiveAcl(target) == AclAdmin; const individualTableEntries = []; const usersAdded: string[] = []; // all shared users being added - organized by denormalized email @@ -450,27 +447,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { usersAdded.sort(this.sortUsers); usersAdded.map(userEmail => { const userKey = `acl-${normalizeEmail(userEmail)}`; - var permission; - if (this.layoutDocAcls){ - if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name; - else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[userKey]); - else permission = StrCast(Doc.GetProto(target)?.[userKey]); - } - else permission = StrCast(target[userKey]); + var permission = StrCast(target[userKey]); individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user }); // adds current user var userEmail = Doc.CurrentUserEmail; const userKey = `acl-${normalizeEmail(userEmail)}`; - if (!usersAdded.includes(userEmail) && userEmail != 'guest' && userEmail != target.author) { + if (userEmail == 'guest') userEmail = 'Public'; + if (!usersAdded.includes(userEmail) && userEmail != 'Public' && userEmail != target.author) { var permission; - if (this.layoutDocAcls){ + if (this.layoutDocAcls) { if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name; else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[userKey]); else permission = StrCast(Doc.GetProto(target)?.[userKey]); - } - else permission = StrCast(target[userKey]); + } else permission = StrCast(target[userKey]); individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user } @@ -480,36 +471,29 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { // adds groups const groupTableEntries: JSX.Element[] = []; const groupList = GroupManager.Instance?.allGroups || []; - groupList.sort(this.sortGroups) + groupList.sort(this.sortGroups); groupList.map(group => { if (group.title != 'Public' && this.selectedDoc) { const groupKey = 'acl-' + normalizeEmail(StrCast(group.title)); if (this.selectedDoc[groupKey] != '' && this.selectedDoc[groupKey] != undefined) { var permission; - if (this.layoutDocAcls){ - if (target[DocAcl][groupKey]){ + if (this.layoutDocAcls) { + if (target[DocAcl][groupKey]) { permission = HierarchyMapping.get(target[DocAcl][groupKey])?.name; - } - else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[groupKey]); + } else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[groupKey]); else permission = StrCast(Doc.GetProto(target)?.[groupKey]); - } - else permission = StrCast(target[groupKey]); + } else permission = StrCast(target[groupKey]); groupTableEntries.unshift(this.sharingItem(StrCast(group.title), showAdmin, permission!, false)); } } }); // public permission - let publicPermission = StrCast(target['acl-Public']); - if (this.layoutDocAcls){ - if (target['acl-Public-layout']) publicPermission = StrCast(target['acl-Public-layout']); - else if (target['embedContainer']) publicPermission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))['acl-Public']); - else StrCast(Doc.GetProto(target)['acl-Public']); - } + const publicPermission = StrCast((this.layoutDocAcls ? target : Doc.GetProto(target))['acl-Public']); return ( <div> - <br/> + <br /> Public / Guest Users <div>{this.colorACLDropDown('Public', showAdmin, publicPermission!, false)}</div> <div> @@ -517,7 +501,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <br></br> Individual Users with Access to this Document{' '} </div> <div className="propertiesView-sharingTable">{<div> {individualTableEntries}</div>}</div> - {groupTableEntries.length>0 ? + {groupTableEntries.length > 0 ? ( <div> <div> {' '} @@ -525,7 +509,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> <div className="propertiesView-sharingTable">{<div> {groupTableEntries}</div>}</div> </div> - : null} + ) : null} </div> ); } diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ce9eb9f17..0daa3dd92 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -8,7 +8,7 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { distributeAcls, inheritParentAcls } from '../../../fields/util'; +import { distributeAcls, GetEffectiveAcl, GetPropAcl, inheritParentAcls } from '../../../fields/util'; import { emptyFunction, incrementTitleCopy } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; @@ -30,7 +30,7 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; import React = require('react'); import { DocumentManager } from '../../util/DocumentManager'; -import { DocAcl } from '../../../fields/DocSymbols'; +import { AclAdmin, AclEdit, DocAcl } from '../../../fields/DocSymbols'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -386,8 +386,13 @@ export class CollectionDockingView extends CollectionSubView() { .map(f => f as Doc); const changesMade = this.props.Document.dockingConfig !== json; if (changesMade) { - this.props.Document.dockingConfig = json; - this.props.Document.data = new List<Doc>(docs); + if (![AclAdmin, AclEdit].includes(GetEffectiveAcl(this.dataDoc))) { + this.layoutDoc.dockingConfig = json; + this.layoutDoc.data = new List<Doc>(docs); + } else { + Doc.SetInPlace(this.rootDoc, 'dockingConfig', json, true); + Doc.SetInPlace(this.rootDoc, 'data', new List<Doc>(docs), true); + } } this._flush?.end(); this._flush = undefined; @@ -521,7 +526,7 @@ export class CollectionDockingView extends CollectionSubView() { _layout_fitWidth: true, title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); - this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); + inheritParentAcls(this.rootDoc, docToAdd); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }); @@ -564,7 +569,7 @@ export class CollectionDockingView extends CollectionSubView() { _freeform_backgroundGrid: true, title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); - this.props.Document.isShared && inheritParentAcls(Doc.GetProto(this.props.Document), Doc.GetProto(docToAdd)); + inheritParentAcls(this.dataDoc, Doc.GetProto(docToAdd)); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }) diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index a2269075d..7767c5b79 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -392,7 +392,7 @@ export class TreeView extends React.Component<TreeViewProps> { const innerAdd = (doc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); - dataIsComputed && Doc.SetContainer(doc, this.doc.embedContainer); + dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); @@ -455,7 +455,7 @@ export class TreeView extends React.Component<TreeViewProps> { const innerAdd = (doc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - dataIsComputed && Doc.SetContainer(doc, this.doc.embedContainer); + dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); @@ -563,7 +563,7 @@ export class TreeView extends React.Component<TreeViewProps> { } const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - !dataIsComputed && added && Doc.SetContainer(doc, this.doc.embedContainer); + !dataIsComputed && added && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index dab269474..a4f5eb62b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -535,7 +535,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // if this is part of a template, let the event go up to the template root unless right/ctrl clicking if ( // prettier-ignore - this.props.isDocumentActive?.() && + (this.props.isDocumentActive?.() || this.props.isContentActive?.()) && !this.props.onBrowseClick?.() && !this.Document.ignoreClick && e.button === 0 && diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 9c138d348..28fbdc192 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -481,7 +481,7 @@ export namespace Doc { doc.embedContainer = container; if (Doc.GetProto(container).author === doc.author) { Object.keys(Doc.GetProto(container)) - .filter(key => key.startsWith('acl')) + .filter(key => key.startsWith('acl') && !key.includes(Doc.CurrentUserEmailNormalized)) .forEach(key => (doc[key] = Doc.GetProto(container)[key])); } } @@ -696,7 +696,7 @@ export namespace Doc { Doc.SetLayout(embedding, Doc.MakeEmbedding(layout)); } embedding.createdFrom = doc; - embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = NumCast(Doc.GetProto(doc).proto_embeddingId) + 1; + embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = DocListCast(Doc.GetProto(doc).proto_embeddings).length + 1; embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`); embedding.author = Doc.CurrentUserEmail; diff --git a/src/fields/util.ts b/src/fields/util.ts index 068323dce..0e9940ced 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -14,6 +14,7 @@ import { List } from './List'; import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; import { RefField } from './RefField'; +import { RichTextField } from './RichTextField'; import { SchemaHeaderField } from './SchemaHeaderField'; import { ComputedField } from './ScriptField'; import { ScriptCast, StrCast } from './Types'; @@ -62,8 +63,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail; - const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAugment || effectiveAcl === AclAdmin || writeMode !== DocServer.WriteMode.LiveReadonly; - const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAugment || effectiveAcl === AclAdmin) && !DocServer.Control.isReadOnly(); + const writeToDoc = + sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField); + const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclAugment && Doc.CurrentUserEmail !== 'guest' && value instanceof RichTextField)) && !DocServer.Control.isReadOnly(); if (writeToDoc) { if (value === undefined) { @@ -104,7 +106,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number ); return true; } - return false; + return true; }); let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl; @@ -128,13 +130,14 @@ export function denormalizeEmail(email: string) { * Copies parent's acl fields to the child */ export function inheritParentAcls(parent: Doc, child: Doc) { - for (const key of Object.keys(Doc.GetProto(parent))) { + if (GetEffectiveAcl(parent) !== AclAdmin) return; + for (const key of Object.keys(parent)) { // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private. - // const permission: string = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : Doc.GetProto(parent)[key]; - const symbol = ReverseHierarchyMap.get(StrCast(Doc.GetProto(parent)[key])); + // const permission: string = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : parent[key]; + const symbol = ReverseHierarchyMap.get(StrCast(parent[key])); if (symbol) { const sharePermission = HierarchyMapping.get(symbol.acl!)!.name; - key.startsWith('acl') && distributeAcls(key, sharePermission, Doc.GetProto(child)); + key.startsWith('acl') && distributeAcls(key, sharePermission, child); } } } @@ -168,30 +171,16 @@ const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, user); }, true); -// return layout acl from cache or chache the acl and return. -const getEffectiveLayoutAclCache = computedFn(function (target: any, user?: string) { - return getEffectiveLayoutAcl(target, user); -}, true); - /** * Calculates the effective access right to a document for the current user. */ export function GetEffectiveAcl(target: any, user?: string): symbol { if (!target) return AclPrivate; if (target[UpdatingFromServer]) return AclAdmin; - return getEffectiveAclCache(Doc.GetProto(target), user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) -} - -/** - * Calculates the effective access layout right to a document for the current user. By getting the container's effective acl if the layout acl isn't set. - */ -export function GetEffectiveLayoutAcl(target: any, user?: string): symbol { - if (!target) return AclPrivate; - if (target[UpdatingFromServer]) return AclAdmin; - return getEffectiveLayoutAclCache(target, user); + return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } -function getPropAcl(target: any, prop: string | symbol | number) { +export function GetPropAcl(target: any, prop: string | symbol | number) { if (typeof prop === 'symbol' || target[UpdatingFromServer]) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable return GetEffectiveAcl(target); @@ -209,13 +198,9 @@ export function SetCachedGroups(groups: string[]) { } function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[DocAcl]; - // if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; + if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group - - // guest dashboard permissions error - if (userChecked == 'guest' && target._type_collection == CollectionViewType.Docking) return AclAugment; - if (targetAcls && Object.keys(targetAcls).length) { let effectiveAcl = AclPrivate; for (const [key, value] of Object.entries(targetAcls)) { @@ -238,37 +223,6 @@ function getEffectiveAcl(target: any, user?: string): symbol { } /** - * Returns the layout acl that is effective on the document passed through as the target. If no layout acls - * have been set, it returns the regular acls for the document target is contained in. - */ -function getEffectiveLayoutAcl(target: any, user?: string): symbol { - const targetAcls = target[DocAcl]; - - const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group - if (targetAcls && Object.keys(targetAcls).length) { - var effectiveAcl; - for (const [key, value] of Object.entries(targetAcls)) { - const entity = denormalizeEmail(key.substring(4)); // an individual or a group - if ((GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') && entity != 'Public') { - if (effectiveAcl && HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) { - effectiveAcl = value as symbol; - } else { - effectiveAcl = value as symbol; - } - } - } - - if (effectiveAcl) { - return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl; - } - } - // authored documents are private until an ACL is set. - const targetAuthor = target.__fieldTuples?.author || target.author; // target may be a Doc or Proxy, so check __fieldTuples.author and .author - if (targetAuthor && targetAuthor !== userChecked) return AclPrivate; - return AclAdmin; -} - -/** * Recursively distributes the access right for a user across the children of a document and its annotations. * @param key the key storing the access right (e.g. acl-groupname) * @param acl the access right being stored (e.g. "Can Edit") @@ -276,56 +230,51 @@ function getEffectiveLayoutAcl(target: any, user?: string): symbol { * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection) * inheritingFromCollection is not currently being used but could be used if acl assignment defaults change */ -export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) { +export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[]) { if (!visited) visited = [] as Doc[]; if (!target || visited.includes(target)) return; if ((target._type_collection === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) { - target[key] = acl; - // if (target !== Doc.GetProto(target)) { - // //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???) - // updateCachedAcls(target); - // } + if (target.author !== Doc.CurrentUserEmail || key !== `acl-${Doc.CurrentUserEmailNormalized}`) { + target[key] = acl; + } + if (target !== Doc.GetProto(target)) { + //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???) + updateCachedAcls(target); + } //return; } visited.push(target); let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive - // taken care of in SharingManager now so that users can decide if they want nested docs changed - // if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) { - // target[key] = acl; - // layoutDocChanged = true; - // if (isDashboard) { - // DocListCastAsync(target[Doc.LayoutFieldKey(target)]).then(docs => { - // docs?.forEach(d => distributeAcls(key, acl, d, inheritingFromCollection, visited)); - // }); - // } - // } + if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) { + if (target.author !== Doc.CurrentUserEmail || key !== `acl-${Doc.CurrentUserEmailNormalized}`) { + target[key] = acl; + layoutDocChanged = true; + } + } let dataDocChanged = false; const dataDoc = target[DocData]; if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) { - - // changing doc proto - but this messes up layout - // if (GetEffectiveAcl(dataDoc) === AclAdmin) { - // dataDoc[key] = acl; - // dataDocChanged = true; - // } + if (GetEffectiveAcl(dataDoc) === AclAdmin && (target.author !== Doc.CurrentUserEmail || key !== `acl-${Doc.CurrentUserEmailNormalized}`)) { + dataDoc[key] = acl; + dataDocChanged = true; + } // maps over the links of the document LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); // maps over the children of the document - // taken care of in SharingManager now so that users can decide if they want nested docs changed - // DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => { - // distributeAcls(key, acl, d, inheritingFromCollection, visited); - // distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited); - // }); + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => { + distributeAcls(key, acl, d, inheritingFromCollection, visited); + d !== d[DocData] && distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited); + }); // maps over the annotations of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); - distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited); + d !== d[DocData] && distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited); }); } @@ -335,7 +284,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : getPropAcl(target, prop); + const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; |