From fa38dbe06d6ddb5f4499b759459a24d2b3c111e8 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 7 Jul 2023 13:36:21 -0400 Subject: a bunch of fixes to simplify collaboration and make it work better. --- src/client/DocServer.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'src/client/DocServer.ts') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 2a7f5a09b..876f2400d 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 } = {}; - 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(/^_/, '')); -- cgit v1.2.3-70-g09d2 From cf88809ea2299395db70ce608c193df7f24f0fb2 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 7 Jul 2023 15:57:51 -0400 Subject: fixed self-ownership of copied Docs created by someone else. prevent dash from being pasted into itself. fixed inheritance of acls --- src/client/DocServer.ts | 6 +- src/client/util/SharingManager.tsx | 109 +++++++++++---------- src/client/views/DocComponent.tsx | 32 ++---- src/client/views/PreviewCursor.tsx | 26 ++--- .../views/collections/CollectionDockingView.tsx | 12 +-- src/fields/Doc.ts | 4 +- src/fields/util.ts | 60 +++++------- 7 files changed, 120 insertions(+), 129 deletions(-) (limited to 'src/client/DocServer.ts') 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) => { - this.permissions = event.currentTarget.value as SharingPermissions; - }; + handlePermissionsChange = undoable( + action((event: React.ChangeEvent) => { + 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<{}> { Share {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}

- -
- +
+
{admin ? (
@@ -585,6 +593,7 @@ export class SharingManager extends React.Component<{}> {
{Doc.noviceMode ? null : (
+ (this.overrideNested = !this.overrideNested))} checked={this.overrideNested} /> (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} />
)} 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

() } 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; 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 -- cgit v1.2.3-70-g09d2 From a60fbbe02f0ff26ef04ce8b44695a82673164270 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 10 Jul 2023 09:32:52 -0400 Subject: updated how Lists and Docs get updated when they contain an ObjectField that will be modified --- src/client/DocServer.ts | 2 +- src/client/views/ScriptingRepl.tsx | 3 +- src/fields/CursorField.ts | 41 ++-- src/fields/Doc.ts | 21 +- src/fields/DocSymbols.ts | 2 +- src/fields/FieldSymbols.ts | 2 +- src/fields/List.ts | 459 +++++++++++++++++-------------------- src/fields/ObjectField.ts | 15 +- src/fields/SchemaHeaderField.ts | 14 +- src/fields/util.ts | 196 +++++++++------- 10 files changed, 378 insertions(+), 377 deletions(-) (limited to 'src/client/DocServer.ts') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 67be96d13..5b452b95b 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 (!Control.isReadOnly()) { + if (!_isReadOnly) { _isReadOnly = true; _CreateField = field => (_cache[field[Id]] = field); _UpdateField = emptyFunction; diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 5dfe10def..2bc2d5e6b 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { DocumentManager } from '../util/DocumentManager'; import { CompileScript, Transformer, ts } from '../util/Scripting'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; +import { undoable } from '../util/UndoManager'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; @@ -161,7 +162,7 @@ export class ScriptingRepl extends React.Component { this.commands.push({ command: this.commandString, result: script.errors }); return; } - const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })); + const result = undoable(() => script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })), 'run:' + this.commandString)(); if (result.success) { this.commands.push({ command: this.commandString, result: result.result }); this.commandsHistory.push(this.commandString); diff --git a/src/fields/CursorField.ts b/src/fields/CursorField.ts index a8a2859d2..46f5a8e1c 100644 --- a/src/fields/CursorField.ts +++ b/src/fields/CursorField.ts @@ -1,44 +1,43 @@ -import { ObjectField } from "./ObjectField"; -import { observable } from "mobx"; -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, createSimpleSchema, object, date } from "serializr"; -import { OnUpdate, ToScriptString, ToString, Copy } from "./FieldSymbols"; +import { ObjectField } from './ObjectField'; +import { observable } from 'mobx'; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, createSimpleSchema, object, date } from 'serializr'; +import { FieldChanged, ToScriptString, ToString, Copy } from './FieldSymbols'; export type CursorPosition = { - x: number, - y: number + x: number; + y: number; }; export type CursorMetadata = { - id: string, - identifier: string, - timestamp: number + id: string; + identifier: string; + timestamp: number; }; export type CursorData = { - metadata: CursorMetadata, - position: CursorPosition + metadata: CursorMetadata; + position: CursorPosition; }; const PositionSchema = createSimpleSchema({ x: true, - y: true + y: true, }); const MetadataSchema = createSimpleSchema({ id: true, identifier: true, - timestamp: true + timestamp: true, }); const CursorSchema = createSimpleSchema({ metadata: object(MetadataSchema), - position: object(PositionSchema) + position: object(PositionSchema), }); -@Deserializable("cursor") +@Deserializable('cursor') export default class CursorField extends ObjectField { - @serializable(object(CursorSchema)) readonly data: CursorData; @@ -50,7 +49,7 @@ export default class CursorField extends ObjectField { setPosition(position: CursorPosition) { this.data.position = position; this.data.metadata.timestamp = Date.now(); - this[OnUpdate]?.(); + this[FieldChanged]?.(); } [Copy]() { @@ -58,9 +57,9 @@ export default class CursorField extends ObjectField { } [ToScriptString]() { - return "invalid"; + return 'invalid'; } [ToString]() { - return "invalid"; + return 'invalid'; } -} \ No newline at end of file +} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 8be295810..698e09915 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -37,11 +37,10 @@ import { Initializing, Self, SelfProxy, - Update, UpdatingFromServer, Width, } from './DocSymbols'; -import { Copy, HandleUpdate, Id, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToScriptString, ToString } from './FieldSymbols'; import { InkField, InkTool } from './InkField'; import { List, ListFieldName } from './List'; import { ObjectField } from './ObjectField'; @@ -52,7 +51,7 @@ import { listSpec } from './Schema'; import { ComputedField, ScriptField } from './ScriptField'; import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types'; import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField'; -import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util'; +import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, containedFieldChangedHandler } from './util'; import JSZip = require('jszip'); export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -337,9 +336,10 @@ export class Doc extends RefField { for (const key in value) { const field = value[key]; field !== undefined && (this[FieldKeys][key] = true); - if (!(field instanceof ObjectField)) continue; - field[Parent] = this[Self]; - field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); + if (field instanceof ObjectField) { + field[Parent] = this[Self]; + field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field); + } } } @@ -360,12 +360,13 @@ export class Doc extends RefField { private [ForceServerWrite]: boolean = false; public [Initializing]: boolean = false; - private [Update] = (diff: any) => { - (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff); - }; - private [Self] = this; private [SelfProxy]: any; + public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => { + if (!this[UpdatingFromServer] || this[ForceServerWrite]) { + DocServer.UpdateField(this[Id], serverOp); + } + }; public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); public [Width] = () => NumCast(this[SelfProxy]._width); public [Height] = () => NumCast(this[SelfProxy]._height); diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts index 65decc147..eab26ed10 100644 --- a/src/fields/DocSymbols.ts +++ b/src/fields/DocSymbols.ts @@ -1,4 +1,4 @@ -export const Update = Symbol('DocUpdate'); +export const DocUpdated = Symbol('DocUpdated'); export const Self = Symbol('DocSelf'); export const SelfProxy = Symbol('DocSelfProxy'); export const FieldKeys = Symbol('DocFieldKeys'); diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts index c381f14f5..0dbeb064b 100644 --- a/src/fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts @@ -1,6 +1,6 @@ export const HandleUpdate = Symbol('FieldHandleUpdate'); export const Id = Symbol('FieldId'); -export const OnUpdate = Symbol('FieldOnUpdate'); +export const FieldChanged = Symbol('FieldChanged'); export const Parent = Symbol('FieldParent'); export const Copy = Symbol('FieldCopy'); export const ToValue = Symbol('FieldToValue'); diff --git a/src/fields/List.ts b/src/fields/List.ts index 033fa569b..183d644d3 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -3,217 +3,15 @@ import { alias, list, serializable } from 'serializr'; import { DocServer } from '../client/DocServer'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { afterDocDeserialize, autoObject, Deserializable } from '../client/util/SerializationHelper'; -import { FieldTuples, Self, SelfProxy, Update } from './DocSymbols'; import { Field } from './Doc'; -import { Copy, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { FieldTuples, Self, SelfProxy } from './DocSymbols'; +import { Copy, FieldChanged, Parent, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; import { listSpec } from './Schema'; import { Cast } from './Types'; -import { deleteProperty, getter, setter, updateFunction } from './util'; - -const listHandlers: any = { - /// Mutator methods - copyWithin() { - throw new Error('copyWithin not supported yet'); - }, - fill(value: any, start?: number, end?: number) { - if (value instanceof RefField) { - throw new Error('fill with RefFields not supported yet'); - } - const res = this[Self].__fieldTuples.fill(value, start, end); - this[Update](); - return res; - }, - pop(): any { - const field = toRealField(this[Self].__fieldTuples.pop()); - this[Update](); - return field; - }, - push: action(function (this: any, ...items: any[]) { - items = items.map(toObjectField); - - const list = this[Self]; - const length = list.__fieldTuples.length; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - //TODO Error checking to make sure parent doesn't already exist - if (item instanceof ObjectField) { - item[Parent] = list; - item[OnUpdate] = updateFunction(list, i + length, item, this); - } - } - const res = list.__fieldTuples.push(...items); - this[Update]({ op: '$addToSet', items, length: length + items.length }); - return res; - }), - reverse() { - const res = this[Self].__fieldTuples.reverse(); - this[Update](); - return res; - }, - shift() { - const res = toRealField(this[Self].__fieldTuples.shift()); - this[Update](); - return res; - }, - sort(cmpFunc: any) { - this[Self].__realFields(); // coerce retrieving entire array - const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); - this[Update](); - return res; - }, - splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) { - this[Self].__realFields(); // coerce retrieving entire array - items = items.map(toObjectField); - const list = this[Self]; - const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount); - for (let i = 0; i < items.length; i++) { - const item = items[i]; - //TODO Error checking to make sure parent doesn't already exist - //TODO Need to change indices of other fields in array - if (item instanceof ObjectField) { - item[Parent] = list; - item[OnUpdate] = updateFunction(list, i + start, item, this); - } - } - let hintArray: { val: any; index: number }[] = []; - for (let i = start; i < start + deleteCount; i++) { - hintArray.push({ val: list.__fieldTuples[i], index: i }); - } - const res = list.__fieldTuples.splice(start, deleteCount, ...items); - // the hint object sends the starting index of the slice and the number - // of elements to delete. - this[Update]( - items.length === 0 && deleteCount - ? { op: '$remFromSet', items: removed, hint: { start: start, deleteCount: deleteCount }, length: list.__fieldTuples.length } - : items.length && !deleteCount && start === list.__fieldTuples.length - ? { op: '$addToSet', items, length: list.__fieldTuples.length } - : undefined - ); - return res.map(toRealField); - }), - unshift(...items: any[]) { - items = items.map(toObjectField); - const list = this[Self]; - const length = list.__fieldTuples.length; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - //TODO Error checking to make sure parent doesn't already exist - //TODO Need to change indices of other fields in array - if (item instanceof ObjectField) { - item[Parent] = list; - item[OnUpdate] = updateFunction(list, i, item, this); - } - } - const res = this[Self].__fieldTuples.unshift(...items); - this[Update](); - return res; - }, - /// Accessor methods - concat: action(function (this: any, ...items: any[]) { - this[Self].__realFields(); - return this[Self].__fieldTuples.map(toRealField).concat(...items); - }), - includes(valueToFind: any, fromIndex: number) { - if (valueToFind instanceof RefField) { - return this[Self].__realFields().includes(valueToFind, fromIndex); - } else { - return this[Self].__fieldTuples.includes(valueToFind, fromIndex); - } - }, - indexOf(valueToFind: any, fromIndex: number) { - if (valueToFind instanceof RefField) { - return this[Self].__realFields().indexOf(valueToFind, fromIndex); - } else { - return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex); - } - }, - join(separator: any) { - this[Self].__realFields(); - return this[Self].__fieldTuples.map(toRealField).join(separator); - }, - lastElement() { - return this[Self].__realFields().lastElement(); - }, - lastIndexOf(valueToFind: any, fromIndex: number) { - if (valueToFind instanceof RefField) { - return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); - } else { - return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); - } - }, - slice(begin: number, end: number) { - this[Self].__realFields(); - return this[Self].__fieldTuples.slice(begin, end).map(toRealField); - }, - - /// Iteration methods - entries() { - return this[Self].__realFields().entries(); - }, - every(callback: any, thisArg: any) { - return this[Self].__realFields().every(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - filter(callback: any, thisArg: any) { - return this[Self].__realFields().filter(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - find(callback: any, thisArg: any) { - return this[Self].__realFields().find(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - findIndex(callback: any, thisArg: any) { - return this[Self].__realFields().findIndex(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - forEach(callback: any, thisArg: any) { - return this[Self].__realFields().forEach(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - map(callback: any, thisArg: any) { - return this[Self].__realFields().map(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - reduce(callback: any, initialValue: any) { - return this[Self].__realFields().reduce(callback, initialValue); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); - }, - reduceRight(callback: any, initialValue: any) { - return this[Self].__realFields().reduceRight(callback, initialValue); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); - }, - some(callback: any, thisArg: any) { - return this[Self].__realFields().some(callback, thisArg); - // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. - // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); - }, - values() { - return this[Self].__realFields().values(); - }, - [Symbol.iterator]() { - return this[Self].__realFields().values(); - }, -}; +import { deleteProperty, getter, setter, containedFieldChangedHandler } from './util'; function toObjectField(field: Field) { return field instanceof RefField ? new ProxyField(field) : field; @@ -223,38 +21,221 @@ function toRealField(field: Field) { return field instanceof ProxyField ? field.value : field; } -function listGetter(target: any, prop: string | symbol, receiver: any): any { - if (listHandlers.hasOwnProperty(prop)) { - return listHandlers[prop]; - } - return getter(target, prop, receiver); -} - -interface ListSpliceUpdate { - type: 'splice'; - index: number; - added: T[]; - removedCount: number; -} - -interface ListIndexUpdate { - type: 'update'; - index: number; - newValue: T; -} - -type ListUpdate = ListSpliceUpdate | ListIndexUpdate; - type StoredType = T extends RefField ? ProxyField : T; export const ListFieldName = 'fields'; @Deserializable('list') class ListImpl extends ObjectField { + static listHandlers: any = { + /// Mutator methods + copyWithin() { + throw new Error('copyWithin not supported yet'); + }, + fill(value: any, start?: number, end?: number) { + if (value instanceof RefField) { + throw new Error('fill with RefFields not supported yet'); + } + const res = this[Self].__fieldTuples.fill(value, start, end); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + pop(): any { + const field = toRealField(this[Self].__fieldTuples.pop()); + this[SelfProxy][FieldChanged]?.(); + return field; + }, + push: action(function (this: ListImpl, ...items: any[]) { + items = items.map(toObjectField); + + const list = this[Self]; + const length = list.__fieldTuples.length; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + //TODO Error checking to make sure parent doesn't already exist + if (item instanceof ObjectField) { + item[Parent] = list; + item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], i + length, item); + } + } + const res = list.__fieldTuples.push(...items); + this[SelfProxy][FieldChanged]?.({ op: '$addToSet', items, length: length + items.length }); + return res; + }), + reverse() { + const res = this[Self].__fieldTuples.reverse(); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + shift() { + const res = toRealField(this[Self].__fieldTuples.shift()); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + sort(cmpFunc: any) { + this[Self].__realFields(); // coerce retrieving entire array + const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) { + this[Self].__realFields(); // coerce retrieving entire array + items = items.map(toObjectField); + const list = this[Self]; + const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + //TODO Error checking to make sure parent doesn't already exist + //TODO Need to change indices of other fields in array + if (item instanceof ObjectField) { + item[Parent] = list; + item[FieldChanged] = containedFieldChangedHandler(this, i + start, item); + } + } + let hintArray: { val: any; index: number }[] = []; + for (let i = start; i < start + deleteCount; i++) { + hintArray.push({ val: list.__fieldTuples[i], index: i }); + } + const res = list.__fieldTuples.splice(start, deleteCount, ...items); + // the hint object sends the starting index of the slice and the number + // of elements to delete. + this[SelfProxy][FieldChanged]?.( + items.length === 0 && deleteCount + ? { op: '$remFromSet', items: removed, hint: { start, deleteCount }, length: list.__fieldTuples.length } + : items.length && !deleteCount && start === list.__fieldTuples.length + ? { op: '$addToSet', items, length: list.__fieldTuples.length } + : undefined + ); + return res.map(toRealField); + }), + unshift(...items: any[]) { + items = items.map(toObjectField); + const list = this[Self]; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + //TODO Error checking to make sure parent doesn't already exist + //TODO Need to change indices of other fields in array + if (item instanceof ObjectField) { + item[Parent] = list; + item[FieldChanged] = containedFieldChangedHandler(this, i, item); + } + } + const res = this[Self].__fieldTuples.unshift(...items); + this[SelfProxy][FieldChanged]?.(); + return res; + }, + /// Accessor methods + concat: action(function (this: any, ...items: any[]) { + this[Self].__realFields(); + return this[Self].__fieldTuples.map(toRealField).concat(...items); + }), + includes(valueToFind: any, fromIndex: number) { + if (valueToFind instanceof RefField) { + return this[Self].__realFields().includes(valueToFind, fromIndex); + } else { + return this[Self].__fieldTuples.includes(valueToFind, fromIndex); + } + }, + indexOf(valueToFind: any, fromIndex: number) { + if (valueToFind instanceof RefField) { + return this[Self].__realFields().indexOf(valueToFind, fromIndex); + } + return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex); + }, + join(separator: any) { + this[Self].__realFields(); + return this[Self].__fieldTuples.map(toRealField).join(separator); + }, + lastElement() { + return this[Self].__realFields().lastElement(); + }, + lastIndexOf(valueToFind: any, fromIndex: number) { + if (valueToFind instanceof RefField) { + return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); + } else { + return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); + } + }, + slice(begin: number, end: number) { + this[Self].__realFields(); + return this[Self].__fieldTuples.slice(begin, end).map(toRealField); + }, + + /// Iteration methods + entries() { + return this[Self].__realFields().entries(); + }, + every(callback: any, thisArg: any) { + return this[Self].__realFields().every(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + filter(callback: any, thisArg: any) { + return this[Self].__realFields().filter(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + find(callback: any, thisArg: any) { + return this[Self].__realFields().find(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + findIndex(callback: any, thisArg: any) { + return this[Self].__realFields().findIndex(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + forEach(callback: any, thisArg: any) { + return this[Self].__realFields().forEach(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + map(callback: any, thisArg: any) { + return this[Self].__realFields().map(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + reduce(callback: any, initialValue: any) { + return this[Self].__realFields().reduce(callback, initialValue); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + }, + reduceRight(callback: any, initialValue: any) { + return this[Self].__realFields().reduceRight(callback, initialValue); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + }, + some(callback: any, thisArg: any) { + return this[Self].__realFields().some(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + values() { + return this[Self].__realFields().values(); + }, + [Symbol.iterator]() { + return this[Self].__realFields().values(); + }, + }; + static listGetter(target: any, prop: string | symbol, receiver: any): any { + if (ListImpl.listHandlers.hasOwnProperty(prop)) { + return ListImpl.listHandlers[prop]; + } + return getter(target, prop, receiver); + } constructor(fields?: T[]) { super(); const list = new Proxy(this, { set: setter, - get: listGetter, + get: ListImpl.listGetter, ownKeys: target => Object.keys(target.__fieldTuples), getOwnPropertyDescriptor: (target, prop) => { if (prop in target[FieldTuples]) { @@ -270,9 +251,9 @@ class ListImpl extends ObjectField { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); - this[SelfProxy] = list; + this[SelfProxy] = list as any as List; // bcz: ugh .. don't know how to convince typesecript that list is a List if (fields) { - (list as any).push(...fields); + this[SelfProxy].push(...fields); } return list; } @@ -305,10 +286,10 @@ class ListImpl extends ObjectField { private set __fieldTuples(value) { this[FieldTuples] = value; for (const key in value) { - const field = value[key]; - if (field instanceof ObjectField) { - field[Parent] = this[Self]; - field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); + const item = value[key]; + if (item instanceof ObjectField) { + item[Parent] = this[Self]; + item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], Number(key), item); } } } @@ -322,16 +303,8 @@ class ListImpl extends ObjectField { // @serializable(alias("fields", list(autoObject()))) @observable private [FieldTuples]: StoredType[] = []; - - private [Update] = (diff: any) => { - // console.log(diff); - const update = this[OnUpdate]; - // update && update(diff); - update?.(diff); - }; - private [Self] = this; - private [SelfProxy]: any; + private [SelfProxy]: List; // also used in utils.ts even though it won't be found using find all references [ToScriptString]() { return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`; diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index daa8a7777..b5bc2952a 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -1,9 +1,14 @@ -import { RefField } from "./RefField"; -import { OnUpdate, Parent, Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { ScriptingGlobals } from "../client/util/ScriptingGlobals"; +import { RefField } from './RefField'; +import { FieldChanged, Parent, Copy, ToScriptString, ToString } from './FieldSymbols'; +import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; +import { Field } from './Doc'; export abstract class ObjectField { - public [OnUpdate]?: (diff?: any) => void; + // prettier-ignore + public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; + items: Field[] | undefined; + length: number | undefined; + hint?: any }, serverOp?: any) => void; public [Parent]?: RefField | ObjectField; abstract [Copy](): ObjectField; @@ -17,4 +22,4 @@ export namespace ObjectField { } } -ScriptingGlobals.add(ObjectField); \ No newline at end of file +ScriptingGlobals.add(ObjectField); diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 4b1855cb0..6dde2e5aa 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -1,7 +1,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, primitive } from 'serializr'; import { ObjectField } from './ObjectField'; -import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols'; +import { Copy, ToScriptString, ToString, FieldChanged } from './FieldSymbols'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; @@ -82,32 +82,32 @@ export class SchemaHeaderField extends ObjectField { setHeading(heading: string) { this.heading = heading; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setColor(color: string) { this.color = color; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setType(type: ColumnType) { this.type = type; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setWidth(width: number) { this.width = width; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setDesc(desc: boolean | undefined) { this.desc = desc; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } setCollapsed(collapsed: boolean | undefined) { this.collapsed = collapsed; - this[OnUpdate]?.(); + this[FieldChanged]?.(); } [Copy]() { diff --git a/src/fields/util.ts b/src/fields/util.ts index 36f619120..4dcbf1fbe 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,15 +1,13 @@ import { $mobx, action, observable, runInAction, trace } from 'mobx'; import { computedFn } from 'mobx-utils'; import { DocServer } from '../client/DocServer'; -import { CollectionViewType } from '../client/documents/DocumentTypes'; import { LinkManager } from '../client/util/LinkManager'; import { SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; import { returnZero } from '../Utils'; -import CursorField from './CursorField'; -import { aclLevel, Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclSelfEdit, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, Update, UpdatingFromServer, Width } from './DocSymbols'; -import { Id, OnUpdate, Parent, ToValue } from './FieldSymbols'; +import { aclLevel, Doc, DocListCast, Field, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols'; +import { FieldChanged, Id, Parent, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; @@ -51,11 +49,11 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number throw new Error("Can't put the same object in multiple documents at the same time"); } value[Parent] = receiver; - value[OnUpdate] = updateFunction(target, prop, value, receiver); + value[FieldChanged] = containedFieldChangedHandler(receiver, prop, value); } if (curValue instanceof ObjectField) { delete curValue[Parent]; - delete curValue[OnUpdate]; + delete curValue[FieldChanged]; } const effectiveAcl = GetEffectiveAcl(target); @@ -81,8 +79,10 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } if (writeToServer) { - if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } }); - else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } }); + // prettier-ignore + if (value === undefined) + (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $unset: { ['fields.' + prop]: '' } }); + else (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) :value}}); if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); @@ -229,7 +229,8 @@ function getEffectiveAcl(target: any, user?: string): symbol { * @param key the key storing the access right (e.g. acl-groupname) * @param acl the access right being stored (e.g. "Can Edit") * @param target the document on which this access right is being set - * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection) + * @param visited list of Doc's already distributed to. + * @param allowUpgrade whether permissions can be made less restrictive * inheritingFromCollection is not currently being used but could be used if acl assignment defaults change */ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean) { @@ -274,6 +275,9 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc dataDocChanged && updateCachedAcls(dataDoc); } +// +// target should be either a Doc or ListImpl. receiver should be a Proxy Or List. +// 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); @@ -344,86 +348,104 @@ export function deleteProperty(target: any, prop: string | number | symbol) { return true; } -export function updateFunction(target: any, prop: any, value: any, receiver: any) { - let lastValue = ObjectField.MakeCopy(value); - return (diff?: any) => { - const op = - diff?.op === '$addToSet' - ? { $addToSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List(diff.items)) } } - : diff?.op === '$remFromSet' - ? { $remFromSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List(diff.items)), hint: diff.hint } } - : { $set: { ['fields.' + prop]: SerializationHelper.Serialize(value) } }; - !op.$set && ((op as any).length = diff.length); - const prevValue = ObjectField.MakeCopy(lastValue as List); - lastValue = ObjectField.MakeCopy(value); - const newValue = ObjectField.MakeCopy(value); - - if (!(value instanceof CursorField) && !value?.some?.((v: any) => v instanceof CursorField)) { - !receiver[UpdatingFromServer] && +// this function creates a function that can be used to setup Undo for whenever an ObjectField changes. +// the idea is that the Doc field setter can only setup undo at the granularity of an entire field and won't even be called if +// just a part of a field (eg. field within an ObjectField) changes. This function returns a function that can be called +// whenever an internal ObjectField field changes. It should be passed a 'diff' specification describing the change. Currently, +// List's are the only true ObjectFields that can be partially modified (ignoring SchemaHeaderFields which should go away). +// The 'diff' specification that a list can send is limited to indicating that something was added, removed, or that the list contents +// were replaced. Based on this specification, an Undo event is setup that will save enough information about the ObjectField to be +// able to undo and redo the partial change. +// +export function containedFieldChangedHandler(container: List | Doc, prop: string | number, liveContainedField: ObjectField) { + let lastValue: FieldResult = liveContainedField instanceof ObjectField ? ObjectField.MakeCopy(liveContainedField) : liveContainedField; + return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => { + const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: Field) => SerializationHelper.Serialize(item)) }); + // prettier-ignore + const serverOp = diff?.op === '$addToSet' + ? { $addToSet: { ['fields.' + prop]: serializeItems() }, length: diff.length } + : diff?.op === '$remFromSet' + ? { $remFromSet: { ['fields.' + prop]: serializeItems(), hint: diff.hint}, length: diff.length } + : { $set: { ['fields.' + prop]: liveContainedField ? SerializationHelper.Serialize(liveContainedField) : undefined } }; + + if (!(container instanceof Doc) || !container[UpdatingFromServer]) { + const prevValue = ObjectField.MakeCopy(lastValue as List); + lastValue = ObjectField.MakeCopy(liveContainedField); + const newValue = ObjectField.MakeCopy(liveContainedField); + if (diff?.op === '$addToSet') { UndoManager.AddEvent( - diff?.op === '$addToSet' - ? { - redo: () => { - console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo - receiver[prop].push(...diff.items.map((item: any) => item.value ?? item)); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - undo: action(() => { - // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - if (item instanceof SchemaHeaderField) { - const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); - ind !== -1 && receiver[prop].splice(ind, 1); - } else { - const ind = receiver[prop].indexOf(item.value ?? item); - ind !== -1 && receiver[prop].splice(ind, 1); - } - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }), - prop: 'add ' + diff.items.length + ' items to list', - } - : diff?.op === '$remFromSet' - ? { - redo: action(() => { - console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item); - ind !== -1 && receiver[prop].splice(ind, 1); - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }), - undo: () => { - // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo - diff.items.forEach((item: any) => { - if (item instanceof SchemaHeaderField) { - const ind = (prevValue as List).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); - ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item); - } else { - const ind = (prevValue as List).indexOf(item.value ?? item); - ind !== -1 && receiver[prop].indexOf(item.value ?? item) === -1 && receiver[prop].splice(ind, 0, item); - } - }); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - prop: 'remove ' + diff.items.length + ' items from list', - } - : { - redo: () => { - console.log('redo list: ' + prop, receiver[prop]); // bcz: uncomment to log undo - receiver[prop] = ObjectField.MakeCopy(newValue as List); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - undo: () => { - // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo - receiver[prop] = ObjectField.MakeCopy(prevValue as List); - lastValue = ObjectField.MakeCopy(receiver[prop]); - }, - prop: 'assign list', - }, + { + redo: () => { + //console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo + (container as any)[prop as any]?.push(...(diff.items || [])?.map((item: any) => item.value ?? item)); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }, + undo: action(() => { + // console.log('undo $add: ' + prop, diff.items); // bcz: uncomment to log undo + diff.items?.forEach((item: any) => { + const ind = + item instanceof SchemaHeaderField // + ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) + : (container as any)[prop as any]?.indexOf(item.value ?? item); + ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1); + }); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }), + prop: 'add ' + diff.items?.length + ' items to list', + }, + diff?.items + ); + } else if (diff?.op === '$remFromSet') { + UndoManager.AddEvent( + { + redo: action(() => { + // console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo + diff.items?.forEach((item: any) => { + const ind = + item instanceof SchemaHeaderField // + ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) + : (container as any)[prop as any]?.indexOf(item.value ?? item); + ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1); + }); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }), + undo: () => { + // console.log('undo $rem: ' + prop, diff.items); // bcz: uncomment to log undo + diff.items?.forEach((item: any) => { + if (item instanceof SchemaHeaderField) { + const ind = (prevValue as List).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); + ind !== -1 && (container as any)[prop as any].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && (container as any)[prop as any].splice(ind, 0, item); + } else { + const ind = (prevValue as List).indexOf(item.value ?? item); + ind !== -1 && (container as any)[prop as any].indexOf(item.value ?? item) === -1 && (container as any)[prop as any].splice(ind, 0, item); + } + }); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }, + prop: 'remove ' + diff.items?.length + ' items from list', + }, diff?.items ); + } else { + const setFieldVal = (val: Field | undefined) => (container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as Field)); + UndoManager.AddEvent( + { + redo: () => { + // console.log('redo list: ' + prop, fieldVal()); // bcz: uncomment to log undo + setFieldVal(newValue instanceof ObjectField ? ObjectField.MakeCopy(newValue) : undefined); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }, + undo: () => { + // console.log('undo list: ' + prop, fieldVal()); // bcz: uncomment to log undo + setFieldVal(prevValue instanceof ObjectField ? ObjectField.MakeCopy(prevValue) : undefined); + lastValue = ObjectField.MakeCopy((container as any)[prop as any]); + }, + prop: 'set list field', + }, + diff?.items + ); + } } - target[Update](op); + container[FieldChanged]?.(undefined, serverOp); }; } -- cgit v1.2.3-70-g09d2 From f93dabed48b4ebd3c7165ee28405c86bb65f2be9 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 11 Jul 2023 16:31:29 -0400 Subject: better way to make 'guest' not write to server. --- src/client/DocServer.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/client/DocServer.ts') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 5b452b95b..fa1fca6ff 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -207,7 +207,7 @@ export namespace DocServer { } export function makeEditable() { - if (Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest') { + if (Control.isReadOnly()) { location.reload(); // _isReadOnly = false; // _CreateField = _CreateFieldImpl; @@ -218,7 +218,7 @@ export namespace DocServer { } export function isReadOnly() { - return _isReadOnly || Doc.CurrentUserEmail === 'guest'; + return _isReadOnly; } } @@ -483,7 +483,7 @@ export namespace DocServer { function _CreateFieldImpl(field: RefField) { _cache[field[Id]] = field; const initialState = SerializationHelper.Serialize(field); - Utils.Emit(_socket, MessageStore.CreateField, initialState); + Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.CreateField, initialState); } let _CreateField: (field: RefField) => void = errorFunc; @@ -503,7 +503,7 @@ export namespace DocServer { } function _UpdateFieldImpl(id: string, diff: any) { - !DocServer.Control.isReadOnly() && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); + !DocServer.Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); } let _UpdateField: (id: string, diff: any) => void = errorFunc; @@ -540,11 +540,11 @@ export namespace DocServer { } export function DeleteDocument(id: string) { - Utils.Emit(_socket, MessageStore.DeleteField, id); + Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.DeleteField, id); } export function DeleteDocuments(ids: string[]) { - Utils.Emit(_socket, MessageStore.DeleteFields, ids); + Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.DeleteFields, ids); } function _respondToDeleteImpl(ids: string | string[]) { -- cgit v1.2.3-70-g09d2