aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/DocServer.ts6
-rw-r--r--src/client/util/SharingManager.tsx109
-rw-r--r--src/client/views/DocComponent.tsx32
-rw-r--r--src/client/views/PreviewCursor.tsx26
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx12
-rw-r--r--src/fields/Doc.ts4
-rw-r--r--src/fields/util.ts60
7 files changed, 120 insertions, 129 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 8b8a9a618..67be96d13 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -198,7 +198,7 @@ export namespace DocServer {
export namespace Control {
let _isReadOnly = false;
export function makeReadOnly() {
- if (!_isReadOnly) {
+ if (!Control.isReadOnly()) {
_isReadOnly = true;
_CreateField = field => (_cache[field[Id]] = field);
_UpdateField = emptyFunction;
@@ -207,7 +207,7 @@ export namespace DocServer {
}
export function makeEditable() {
- if (_isReadOnly) {
+ if (Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest') {
location.reload();
// _isReadOnly = false;
// _CreateField = _CreateFieldImpl;
@@ -218,7 +218,7 @@ export namespace DocServer {
}
export function isReadOnly() {
- return _isReadOnly;
+ return _isReadOnly || Doc.CurrentUserEmail === 'guest';
}
}
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index d3241009a..828271270 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,4 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Colors } from 'browndash-components';
import { concat, intersection } from 'lodash';
import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -22,6 +23,7 @@ import { GroupManager, UserOptions } from './GroupManager';
import { GroupMemberView } from './GroupMemberView';
import { SelectionManager } from './SelectionManager';
import './SharingManager.scss';
+import { undoable } from './UndoManager';
export interface User {
email: string;
@@ -76,8 +78,10 @@ export class SharingManager extends React.Component<{}> {
@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 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 layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
@observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not
+ @observable private _buttonDown = false;
// private get linkVisible() {
// return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
@@ -91,6 +95,7 @@ export class SharingManager extends React.Component<{}> {
DictationOverlay.Instance.hasActiveModal = true;
this.isOpen = this.targetDoc !== undefined;
this.permissions = SharingPermissions.Augment;
+ this.overrideNested = true;
});
};
@@ -149,36 +154,30 @@ export class SharingManager extends React.Component<{}> {
/**
* Shares the document with a user.
*/
- setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => {
+ setInternalSharing = undoable((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 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);
-
- distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined);
+ distributeAcls(acl, permission as SharingPermissions, doc);
if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc);
else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc);
});
- };
+ }, 'set Doc permissions');
/**
* Sets the permission on the target for the group.
* @param group
* @param permission
*/
- setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
+ setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
const target = targetDoc || this.targetDoc!;
- const key = normalizeEmail(StrCast(group.title));
- let acl = `acl-${key}`;
+ const acl = `acl-${normalizeEmail(StrCast(group.title))}`;
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);
-
- distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined);
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.overrideNested);
if (group instanceof Doc) {
Doc.AddDocToList(group, 'docsShared', doc);
@@ -191,7 +190,7 @@ export class SharingManager extends React.Component<{}> {
});
}
});
- };
+ }, 'set group permissions');
/**
* Shares the documents shared with a group with a new user who has been added to that group.
@@ -217,23 +216,23 @@ export class SharingManager extends React.Component<{}> {
/**
* Called from the properties sidebar to change permissions of a user.
*/
- shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => {
+ shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => {
if (layout) this.layoutDocAcls = true;
if (shareWith !== 'Public' && shareWith !== 'Override') {
const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? Doc.CurrentUserEmail : shareWith));
docs.forEach(doc => {
if (user) this.setInternalSharing(user, permission, doc);
- else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc);
+ else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true);
});
} else {
docs.forEach(doc => {
if (GetEffectiveAcl(doc) === AclAdmin) {
- distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined);
+ distributeAcls(`acl-${shareWith}`, permission, doc, undefined, true);
}
});
}
this.layoutDocAcls = false;
- };
+ }, 'sidebar set permissions');
/**
* Removes the documents shared with a user through a group when the user is removed from the group.
@@ -261,7 +260,7 @@ export class SharingManager extends React.Component<{}> {
if (group.docsShared) {
DocListCast(group.docsShared).forEach(doc => {
const acl = `acl-${StrCast(group.title)}`;
- distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined);
+ distributeAcls(acl, SharingPermissions.None, doc);
const members: string[] = JSON.parse(StrCast(group.members));
const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
@@ -342,39 +341,43 @@ export class SharingManager extends React.Component<{}> {
/**
* Handles changes in the permission chosen to share with someone with
*/
- @action
- handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
- this.permissions = event.currentTarget.value as SharingPermissions;
- };
+ handlePermissionsChange = undoable(
+ action((event: React.ChangeEvent<HTMLSelectElement>) => {
+ this.permissions = event.currentTarget.value as SharingPermissions;
+ }),
+ 'permission change'
+ );
/**
* Calls the relevant method for sharing, displays the popup, and resets the relevant variables.
*/
- @action
- share = () => {
- 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, undefined);
- } else {
- this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
- }
- });
-
- const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
- TaskCompletionBox.popupX = left - 1.5 * width;
- TaskCompletionBox.popupY = top - 1.5 * height;
- TaskCompletionBox.textDisplayed = 'Document shared!';
- TaskCompletionBox.taskCompleted = true;
- setTimeout(
- action(() => (TaskCompletionBox.taskCompleted = false)),
- 2000
- );
+ share = undoable(
+ action(() => {
+ 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, undefined);
+ } else {
+ this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
+ }
+ });
+
+ const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
+ TaskCompletionBox.popupX = left - 1.5 * width;
+ TaskCompletionBox.popupY = top - 1.5 * height;
+ TaskCompletionBox.textDisplayed = 'Document shared!';
+ TaskCompletionBox.taskCompleted = true;
+ setTimeout(
+ action(() => (TaskCompletionBox.taskCompleted = false)),
+ 2000
+ );
- this.layoutDocAcls = false;
- this.selectedUsers = null;
- }
- };
+ this.layoutDocAcls = false;
+ this.selectedUsers = null;
+ }
+ }),
+ 'share Doc'
+ );
/**
* Sorting algorithm to sort users.
@@ -542,12 +545,17 @@ export class SharingManager extends React.Component<{}> {
<b>Share </b>
{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}
</p>
- <button className="share-copy-link" onClick={this.copyURL}>
- <FontAwesomeIcon title={'Copy Public URL'} icon={'copy'} size={'sm'} onClick={this.copyURL} />
+ <button
+ className="share-copy-link"
+ style={{ background: this._buttonDown ? Colors.LIGHT_BLUE : undefined }}
+ onPointerDown={action(e => (this._buttonDown = true))}
+ onPointerUp={action(e => (this._buttonDown = false))}
+ onClick={this.copyURL}>
+ <FontAwesomeIcon title="Copy Public URL" icon="copy" size="sm" />
&nbsp; Copy Public URL
</button>
- <div className={'close-button'} onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} />
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon="times" color="black" size="lg" />
</div>
{admin ? (
<div className="share-container">
@@ -585,6 +593,7 @@ 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>
)}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 44af51341..422d2d6d7 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -190,36 +190,24 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
}
const added = docs;
if (added.length) {
- const aclKeys = Object.keys(Doc.GetProto(this.props.Document)[DocAcl] ?? {});
-
- GetEffectiveAcl(this.rootDoc) === AclAdmin &&
- aclKeys.forEach(key =>
+ Object.keys(Doc.GetProto(this.rootDoc)[DocAcl]) // apply all collection acls (except pseudo-acl 'Me') to each added doc
+ .filter(key => key !== 'acl-Me')
+ .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));
- }
+ 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, d);
})
);
- if (effectiveAcl === AclAugment) {
+ if ([AclAugment, AclEdit, AclAdmin].includes(effectiveAcl)) {
added.map(doc => {
+ doc._dragOnlyWithinContainer = undefined;
if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc;
- Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
- Doc.SetContainer(doc, targetDataDoc);
+ Doc.SetContainer(doc, this.rootDoc);
inheritParentAcls(targetDataDoc, doc);
});
- } else {
- added
- .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)))
- .map(doc => {
- doc._dragOnlyWithinContainer = undefined;
- if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc;
- Doc.SetContainer(doc, this.rootDoc);
- inheritParentAcls(targetDataDoc, doc);
- });
const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add)));
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index b513fe245..82d2bff56 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -50,18 +50,20 @@ export class PreviewCursor extends React.Component<{}> {
PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, PreviewCursor._addDocument).then(batch.end);
} else if (re.test(plain)) {
const url = plain;
- undoBatch(() =>
- PreviewCursor._addDocument(
- Docs.Create.WebDocument(url, {
- title: url,
- _width: 500,
- _height: 300,
- data_useCors: true,
- x: newPoint[0],
- y: newPoint[1],
- })
- )
- )();
+ if (url.startsWith(window.location.href)) {
+ undoBatch(() =>
+ PreviewCursor._addDocument(
+ Docs.Create.WebDocument(url, {
+ title: url,
+ _width: 500,
+ _height: 300,
+ data_useCors: true,
+ x: newPoint[0],
+ y: newPoint[1],
+ })
+ )
+ )();
+ } else alert('cannot paste dash into itself');
} else if (plain.startsWith('__DashDocId(') || plain.startsWith('__DashCloneId(')) {
const clone = plain.startsWith('__DashCloneId(');
const docids = plain.split(':');
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 0daa3dd92..30bc8cbec 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -500,15 +500,15 @@ export class CollectionDockingView extends CollectionSubView() {
}
};
tabCreated = (tab: any) => {
- const aclKeys = Object.keys(Doc.GetProto(this.props.Document)[DocAcl] ?? {});
- aclKeys.forEach(key => {
- if (key != 'acl-Me') {
+ const aclKeys = Object.keys(Doc.GetProto(this.rootDoc)[DocAcl] ?? {});
+ aclKeys
+ .filter(key => key !== 'acl-Me')
+ .forEach(key => {
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(tab));
- }
- });
+ distributeAcls(key, permission, tab);
+ });
this.tabMap.add(tab);
tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
};
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 28fbdc192..8be295810 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -751,7 +751,9 @@ export namespace Doc {
}
};
const docAtKey = doc[key];
- if (docAtKey instanceof Doc) {
+ if (key === 'author') {
+ assignKey(Doc.CurrentUserEmail);
+ } else if (docAtKey instanceof Doc) {
if (pruneDocs.includes(docAtKey)) {
// prune doc and do nothing
} else if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || ['embedContainer', 'annotationOn', 'proto'].includes(key) || ((key === 'link_anchor_1' || key === 'link_anchor_2') && doc.author === Doc.CurrentUserEmail))) {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 0e9940ced..815f3b186 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -65,7 +65,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail;
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();
+ const writeToServer =
+ !DocServer.Control.isReadOnly() && //
+ (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclAugment && value instanceof RichTextField));
if (writeToDoc) {
if (value === undefined) {
@@ -230,52 +232,40 @@ function getEffectiveAcl(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[]) {
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean) {
+ const selfKey = `acl-${Doc.CurrentUserEmailNormalized}`;
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)) {
- 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;
- }
+ if (!target || visited.includes(target) || key === selfKey) 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
- 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)!)) {
- if (GetEffectiveAcl(dataDoc) === AclAdmin && (target.author !== Doc.CurrentUserEmail || key !== `acl-${Doc.CurrentUserEmailNormalized}`)) {
- dataDoc[key] = acl;
- dataDocChanged = true;
- }
+ if (dataDoc && (allowUpgrade || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) {
+ // propagate ACLs to links, children, and annotations
- // maps over the links of the document
- LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
+ LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, visited, allowUpgrade));
- // maps over the children of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => {
- distributeAcls(key, acl, d, inheritingFromCollection, visited);
- d !== d[DocData] && distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited);
+ distributeAcls(key, acl, d, visited, allowUpgrade);
+ d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade);
});
- // maps over the annotations of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => {
- distributeAcls(key, acl, d, inheritingFromCollection, visited);
- d !== d[DocData] && distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited);
+ distributeAcls(key, acl, d, visited, allowUpgrade);
+ d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade);
});
+
+ if (GetEffectiveAcl(dataDoc) === AclAdmin) {
+ dataDoc[key] = acl;
+ dataDocChanged = true;
+ }
+ }
+
+ 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) allowUpgrade is set B) the key doesn't already exist or c) the right being inherited is more restrictive
+ if (GetEffectiveAcl(target) === AclAdmin && (allowUpgrade || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) {
+ target[key] = acl;
+ layoutDocChanged = true;
}
layoutDocChanged && updateCachedAcls(target); // updates target[AclSym] when changes to acls have been made