diff options
26 files changed, 328 insertions, 216 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index bac324c77..8ded43468 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,13 +1,14 @@ import * as io from 'socket.io-client'; import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message"; -import { Opt, Doc, fetchProto } from '../fields/Doc'; +import { Opt, Doc, fetchProto, FieldsSym } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../fields/RefField'; -import { Id, HandleUpdate } from '../fields/FieldSymbols'; +import { Id, HandleUpdate, Parent } from '../fields/FieldSymbols'; import GestureOverlay from './views/GestureOverlay'; import MobileInkOverlay from '../mobile/MobileInkOverlay'; import { runInAction } from 'mobx'; +import { ObjectField } from '../fields/ObjectField'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -207,12 +208,12 @@ export namespace DocServer { * the server if the document has not been cached. * @param id the id of the requested document */ - const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => { + const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => { // an initial pass through the cache to determine whether the document needs to be fetched, // is already in the process of being fetched or already exists in the // cache const cached = _cache[id]; - if (cached === undefined) { + if (cached === undefined || force) { // NOT CACHED => we'll have to send a request to the server // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string) @@ -226,7 +227,16 @@ export namespace DocServer { const deserializeField = getSerializedField.then(async fieldJson => { // deserialize const field = await SerializationHelper.Deserialize(fieldJson); - if (field !== undefined) { + if (force && field instanceof Doc && cached instanceof Doc) { + Array.from(Object.keys(field)).forEach(key => { + const fieldval = field[key]; + if (fieldval instanceof ObjectField) { + fieldval[Parent] = undefined; + } + cached[key] = field[key]; + }); + } + else if (field !== undefined) { _cache[id] = field; } else { delete _cache[id]; @@ -238,8 +248,8 @@ export namespace DocServer { }); // here, indicate that the document associated with this id is currently // being retrieved and cached - _cache[id] = deserializeField; - return deserializeField; + !force && (_cache[id] = deserializeField); + return force ? cached as any : deserializeField; } else if (cached instanceof Promise) { // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s), // and requested the document I'm looking for. Shouldn't fetch again, just @@ -260,11 +270,11 @@ export namespace DocServer { } }; - let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc; + let _GetRefField: (id: string, force: boolean) => Promise<Opt<RefField>> = errorFunc; let _GetCachedRefField: (id: string) => Opt<RefField> = errorFunc; - export function GetRefField(id: string): Promise<Opt<RefField>> { - return _GetRefField(id); + export function GetRefField(id: string, force = false): Promise<Opt<RefField>> { + return _GetRefField(id, force); } export function GetCachedRefField(id: string): Opt<RefField> { return _GetCachedRefField(id); @@ -330,29 +340,35 @@ export namespace DocServer { const proms: Promise<void>[] = []; runInAction(() => { for (const field of fields) { - if (field !== undefined && field !== null) { + if (field !== undefined && field !== null && !_cache[field.id]) { // deserialize - const prom = SerializationHelper.Deserialize(field).then(deserialized => { - fieldMap[field.id] = deserialized; - - //overwrite or delete any promises (that we inserted as flags - // to indicate that the field was in the process of being fetched). Now everything - // should be an actual value within or entirely absent from the cache. - if (deserialized !== undefined) { - _cache[field.id] = deserialized; - } else { - delete _cache[field.id]; - } - return deserialized; - }); - // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) - // we set the value at the field's id to a promise that will resolve to the field. - // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). - // The mapping in the .then call ensures that when other callers await these promises, they'll - // get the resolved field - _cache[field.id] = prom; - // adds to a list of promises that will be awaited asynchronously - proms.push(prom); + const cached = _cache[field.id]; + if (!cached) { + const prom = SerializationHelper.Deserialize(field).then(deserialized => { + fieldMap[field.id] = deserialized; + + //overwrite or delete any promises (that we inserted as flags + // to indicate that the field was in the process of being fetched). Now everything + // should be an actual value within or entirely absent from the cache. + if (deserialized !== undefined) { + _cache[field.id] = deserialized; + } else { + delete _cache[field.id]; + } + return deserialized; + }); + // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) + // we set the value at the field's id to a promise that will resolve to the field. + // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). + // The mapping in the .then call ensures that when other callers await these promises, they'll + // get the resolved field + _cache[field.id] = prom; + + // adds to a list of promises that will be awaited asynchronously + proms.push(prom); + } else if (cached instanceof Promise) { + proms.push(cached as any); + } } } }); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index b47cfb33a..6d752832a 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -346,6 +346,14 @@ export class CurrentUserUtils { iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF); doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView); } + if (doc["template-icon-view-button"] === undefined) { + const iconBtnView = Docs.Create.FontIconDocument({ + title: "icon_" + DocumentType.BUTTON, _nativeHeight: 30, _nativeWidth: 30, + _width: 30, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") + }); + iconBtnView.isTemplateDoc = makeTemplate(iconBtnView, true, "icon_" + DocumentType.BUTTON); + doc["template-icon-view-button"] = new PrefetchProxy(iconBtnView); + } if (doc["template-icon-view-img"] === undefined) { const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { title: "data", _width: 50, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") @@ -359,11 +367,11 @@ export class CurrentUserUtils { doc["template-icon-view-col"] = new PrefetchProxy(iconColView); } if (doc["template-icons"] === undefined) { - doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, + doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc, doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc], { title: "icon templates", _height: 75 })); } else { const templateIconsDoc = Cast(doc["template-icons"], Doc, null); - const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, + const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc, doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; DocListCastAsync(templateIconsDoc.data).then(async curIcons => { await Promise.all(curIcons!); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index f6fc13c98..237ea7675 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -152,18 +152,18 @@ export class DocumentManager { const first = getFirstDocView(annotatedDoc); if (first) { annotatedDoc = first.props.Document; - if (docView) { - docView.props.focus(annotatedDoc, false); - } + docView?.props.focus(annotatedDoc, false); } } if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? - if (originatingDoc?.isPushpin) docView.props.Document.hidden = !docView.props.Document.hidden; + if (originatingDoc?.isPushpin) { + docView.props.Document.hidden = !docView.props.Document.hidden; + } else { docView.props.Document.hidden && (docView.props.Document.hidden = undefined); docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish); + highlight(); } - highlight(); } else { const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined; const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined; @@ -218,25 +218,27 @@ export class DocumentManager { const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : (traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs); - const linkDoc = linkDocList.length && linkDocList[0]; - if (linkDoc) { - const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 : - (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; - const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") : - doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") : - (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number"))); - if (target) { - const containerDoc = (await Cast(target.annotationOn, Doc)) || target; - containerDoc.currentTimecode = targetTimecode; - const targetContext = await target?.context as Doc; - const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc, undefined, doc, finished); + const followLinks = linkDocList.length ? (doc.isPushpin ? linkDocList : [linkDocList[0]]) : []; + followLinks.forEach(async linkDoc => { + if (linkDoc) { + const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 : + (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; + const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") : + doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") : + (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number"))); + if (target) { + const containerDoc = (await Cast(target.annotationOn, Doc)) || target; + containerDoc.currentTimecode = targetTimecode; + const targetContext = await target?.context as Doc; + const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc, undefined, doc, finished); + } else { + finished?.(); + } } else { finished?.(); } - } else { - finished?.(); - } + }); } } Scripting.addGlobal(function DocFocus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); });
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 837f0b1db..4b1860b5c 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -224,11 +224,10 @@ export namespace DragManager { } // drag a button template and drop a new button - export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { + export function + StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { const finishDrag = (e: DragCompleteEvent) => { - const bd = params.length > Object.keys(vars).length ? - Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) }) : - Docs.Create.FontIconDocument({ toolTip: title, z: 1, _nativeWidth: 30, _nativeHeight: 30, _width: 30, _height: 30, title, onClick: ScriptField.MakeScript(script) }); + const bd = Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) }); params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); // copy all "captured" arguments into document parameterfields initialize?.(bd); Doc.GetProto(bd)["onClick-paramFieldKeys"] = new List<string>(params); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index f9837298d..d0acf14c3 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -57,7 +57,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data?.draggedDocuments.map((doc, i) => { let dbox = doc; // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant - if (doc.type === DocumentType.FONTICON) { + if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes("FontIconBox")) { dbox = Doc.MakeAlias(doc); } else if (!doc.onDragStart && !doc.isButtonBar) { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 2e5ecc543..72fba5c1b 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -16,6 +16,7 @@ import { StrCast, Cast } from "../../fields/Types"; import GroupMemberView from "./GroupMemberView"; import { setGroups } from "../../fields/util"; import { DocServer } from "../DocServer"; +import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle); @@ -36,11 +37,13 @@ export default class GroupManager extends React.Component<{}> { @observable currentGroup: Opt<Doc>; // the currently selected group. @observable private createGroupModalOpen: boolean = false; private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. + private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); private currentUserGroups: string[] = []; @observable private buttonColour: "#979797" | "black" = "#979797"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; + constructor(props: Readonly<{}>) { super(props); GroupManager.Instance = this; @@ -48,6 +51,7 @@ export default class GroupManager extends React.Component<{}> { componentDidMount() { this.populateUsers(); + this.populateGroups(); } /** @@ -74,6 +78,17 @@ export default class GroupManager extends React.Component<{}> { return Promise.all(evaluating); } + populateGroups = () => { + DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { + groups?.forEach(group => { + const members: string[] = JSON.parse(StrCast(group.members)); + if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName)); + }); + + setGroups(this.currentUserGroups); + }); + } + /** * @returns the options to be rendered in the dropdown menu to add users and create a group. */ @@ -89,14 +104,7 @@ export default class GroupManager extends React.Component<{}> { SelectionManager.DeselectAll(); this.isOpen = true; this.populateUsers(); - DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { - groups?.forEach(group => { - const members: string[] = JSON.parse(StrCast(group.members)); - if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName)); - }); - - setGroups(this.currentUserGroups); - }); + this.populateGroups(); } /** @@ -298,6 +306,14 @@ export default class GroupManager extends React.Component<{}> { this.selectedUsers = null; this.inputRef.current.value = ""; this.buttonColour = "#979797"; + + const { left, width, top } = this.createGroupButtonRef.current!.getBoundingClientRect(); + TaskCompletionBox.popupX = left - 2 * width; + TaskCompletionBox.popupY = top; + TaskCompletionBox.textDisplayed = "Group created!"; + TaskCompletionBox.taskCompleted = true; + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000); + } private get groupCreationModal() { @@ -340,7 +356,9 @@ export default class GroupManager extends React.Component<{}> { }) }} /> - <button onClick={this.createGroup} + <button + ref={this.createGroupButtonRef} + onClick={this.createGroup} style={{ background: this.buttonColour }} disabled={this.buttonColour === "#979797"} > diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 9c857a7c0..452a58d21 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -20,7 +20,8 @@ import GroupMemberView from "./GroupMemberView"; import Select from "react-select"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { List } from "../../fields/List"; -import { distributeAcls } from "../../fields/util"; +import { distributeAcls, SharingPermissions } from "../../fields/util"; +import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; library.add(fa.faCopy, fa.faTimes); @@ -29,13 +30,6 @@ export interface User { userDocumentId: string; } -export enum SharingPermissions { - Edit = "Can Edit", - Add = "Can Add", - View = "Can View", - None = "Not Shared" -} - interface GroupOptions { label: string; options: UserOptions[]; @@ -69,6 +63,8 @@ export default class SharingManager extends React.Component<{}> { @observable private permissions: SharingPermissions = SharingPermissions.Edit; @observable private individualSort: "ascending" | "descending" | "none" = "none"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; + private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); + // private get linkVisible() { @@ -90,6 +86,8 @@ export default class SharingManager extends React.Component<{}> { public close = action(() => { this.isOpen = false; this.users = []; + this.selectedUsers = null; + setTimeout(action(() => { // this.copied = false; DictationOverlay.Instance.hasActiveModal = false; @@ -235,7 +233,7 @@ export default class SharingManager extends React.Component<{}> { private get sharingOptions() { return Object.values(SharingPermissions).map(permission => { return ( - <option key={permission} value={permission}> + <option key={permission} value={permission} selected={permission === SharingPermissions.Edit}> {permission} </option> ); @@ -284,15 +282,25 @@ export default class SharingManager extends React.Component<{}> { @action share = () => { - this.selectedUsers?.forEach(user => { - if (user.value.includes(indType)) { - this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions); - } - else { - this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); - } - }); - this.selectedUsers = null; + 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); + } + 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 - height; + TaskCompletionBox.textDisplayed = "Document shared!"; + TaskCompletionBox.taskCompleted = true; + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000); + + this.selectedUsers = null; + } } sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => { @@ -456,7 +464,7 @@ export default class SharingManager extends React.Component<{}> { <select className="permissions-select" onChange={this.handlePermissionsChange}> {this.sharingOptions} </select> - <button className="share-button" onClick={this.share}> + <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> Share </button> </div> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 95c1bcda8..cfabae8a1 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -7,8 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils'; import { List } from '../../fields/List'; import { DateField } from '../../fields/DateField'; import { ScriptField } from '../../fields/ScriptField'; -import { GetEffectiveAcl, getPlaygroundMode } from '../../fields/util'; -import { SharingPermissions } from '../util/SharingManager'; +import { GetEffectiveAcl, getPlaygroundMode, SharingPermissions } from '../../fields/util'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -151,7 +150,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T const effectiveAcl = GetEffectiveAcl(this.dataDoc); if (added.length) { - if (effectiveAcl === AclReadonly && !getPlaygroundMode()) { + if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) { return false; } else { diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 5948ada88..424a06431 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -157,6 +157,7 @@ $linkGap : 3px; grid-column-end: 2; pointer-events: all; padding-left: 5px; + cursor: pointer; } .documentDecorations-title { @@ -204,7 +205,7 @@ $linkGap : 3px; width: 20px; } -.documentDecorations-closeButton { +.documentDecorations-openInTab { opacity: 1; grid-column-start: 4; grid-column-end: 5; @@ -216,7 +217,7 @@ $linkGap : 3px; margin-top: auto; } -.documentDecorations-minimizeButton { +.documentDecorations-closeButton { opacity: 1; grid-column-start: 1; grid-column-end: 3; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index fec4ad9e0..8d63537e7 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -438,8 +438,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (nwidth / nheight !== width / height) { height = nheight / nwidth * width; } - if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; - else dW = dH * nwidth / nheight; + if (!e.ctrlKey) { + if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; + else dW = dH * nwidth / nheight; + } } const actualdW = Math.max(width + (dW * scale), 20); const actualdH = Math.max(height + (dH * scale), 20); @@ -463,20 +465,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } else if (nwidth > 0 && nheight > 0) { if (Math.abs(dW) > Math.abs(dH)) { - if (!fixedAspect) { + if (!fixedAspect || e.ctrlKey) { doc._nativeWidth = actualdW / (doc._width || 1) * (doc._nativeWidth || 0); } doc._width = actualdW; if (fixedAspect && !doc._fitWidth) doc._height = nheight / nwidth * doc._width; - else doc._height = actualdH; + else if (!fixedAspect || !e.ctrlKey) doc._height = actualdH; } else { - if (!fixedAspect) { + if (!fixedAspect || e.ctrlKey) { doc._nativeHeight = actualdH / (doc._height || 1) * (doc._nativeHeight || 0); } doc._height = actualdH; if (fixedAspect && !doc._fitWidth) doc._width = nwidth / nheight * doc._height; - else doc._width = actualdW; + else if (!fixedAspect || !e.ctrlKey) doc._width = actualdW; } } else { dW && (doc._width = actualdW); @@ -549,8 +551,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> </div></Tooltip>) : ( - <Tooltip title={<><div className="dash-tooltip">Iconify</div></>} placement="top"> - <div className="documentDecorations-minimizeButton" onClick={this.onCloseClick}> + <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top"> + <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> </div></Tooltip>); @@ -616,7 +618,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div className="documentDecorations-iconifyButton" onPointerDown={this.onIconifyDown}> {"_"} </div></Tooltip>} - <Tooltip title={<><div className="dash-tooltip">Open Document In Tab</div></>} placement="top"><div className="documentDecorations-closeButton" onPointerDown={this.onMaximizeDown}> + <Tooltip title={<><div className="dash-tooltip">Open Document In Tab</div></>} placement="top"><div className="documentDecorations-openInTab" onPointerDown={this.onMaximizeDown}> {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} </div></Tooltip> <div id="documentDecorations-rotation" className="documentDecorations-rotation" diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index e620f1d36..b24ecc7ea 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -8,7 +8,7 @@ import { faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTimesCircle, faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight, faHeading, faRulerCombined, faFillDrip, faLink, faUnlink, faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, - faPaintRoller, faBars, faBrush, faShapes, faEllipsisH, faHandPaper, faAngleDown, faAngleUp, faSearchPlus, faPlayCircle, faClock, faRocket, faExchangeAlt, faMap + faPaintRoller, faBars, faBrush, faShapes, faEllipsisH, faHandPaper, faAngleDown, faAngleUp, faSearchPlus, faPlayCircle, faClock, faRocket, faExchangeAlt, faMap, faUser } from '@fortawesome/free-solid-svg-icons'; import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -61,7 +61,7 @@ import { DocumentManager } from '../util/DocumentManager'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { LinkMenu } from './linking/LinkMenu'; import { LinkDocPreview } from './nodes/LinkDocPreview'; -import { LinkCreatedBox } from './nodes/LinkCreatedBox'; +import { TaskCompletionBox } from './nodes/TaskCompletedBox'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager'; @@ -150,7 +150,7 @@ export class MainView extends React.Component { faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTrashAlt, faAngleRight, faBell, faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight, faHeading, faRulerCombined, faFillDrip, faLink, faUnlink, faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, - faPaintRoller, faBars, faBrush, faShapes, faEllipsisH, faHandPaper, faAngleDown, faAngleUp, faSearchPlus, faPlayCircle, faClock, faRocket, faExchangeAlt, faMap); + faPaintRoller, faBars, faBrush, faShapes, faEllipsisH, faHandPaper, faAngleDown, faAngleUp, faSearchPlus, faPlayCircle, faClock, faRocket, faExchangeAlt, faMap, faUser); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -623,7 +623,7 @@ export class MainView extends React.Component { {this.mainContent} </GestureOverlay> <PreviewCursor /> - <LinkCreatedBox /> + <TaskCompletionBox /> <ContextMenu /> <FormatShapePane /> <RadialMenu /> diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 992c1f600..81c349b26 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction, Lambda } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, Field } from "../../../fields/Doc"; import { BoolCast, Cast, StrCast, NumCast } from "../../../fields/Types"; import AntimodeMenu from "../AntimodeMenu"; import "./CollectionMenu.scss"; @@ -24,6 +24,7 @@ import { Document } from "../../../fields/documentSchemas"; import { SelectionManager } from "../../util/SelectionManager"; import { DocumentView } from "../nodes/DocumentView"; import { ColorState } from "react-color"; +import { ObjectField } from "../../../fields/ObjectField"; @observer export default class CollectionMenu extends AntimodeMenu { @@ -72,42 +73,48 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp get target() { return this.props.CollectionView.props.Document; } _templateCommand = { params: ["target", "source"], title: "item view", - script: "this.target.childLayoutTemplate = getDocTemplate(this.source?.[0])", + script: "self.target.childLayoutTemplate = getDocTemplate(self.source?.[0])", immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayoutTemplate = Doc.getDocTemplate(source?.[0]))), initialize: emptyFunction, }; _narrativeCommand = { params: ["target", "source"], title: "child click view", - script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])", + script: "self.target.childClickedOpenTemplateView = getDocTemplate(self.source?.[0])", immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))), initialize: emptyFunction, }; _contentCommand = { params: ["target", "source"], title: "clear content", - script: "getProto(this.target).data = copyField(this.source);", + script: "getProto(self.target).data = copyField(self.source);", immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source), initialize: emptyFunction, }; _viewCommand = { params: ["target"], title: "bookmark view", - script: "this.target._panX = this['target-panX']; this.target._panY = this['target-panY']; this.target._viewScale = this['target-viewScale'];", + script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale'];", immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; }), initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; }, }; _clusterCommand = { params: ["target"], title: "fit content", - script: "this.target._fitToBox = !this.target._fitToBox;", + script: "self.target._fitToBox = !self.target._fitToBox;", immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox), initialize: emptyFunction }; _fitContentCommand = { params: ["target"], title: "toggle clusters", - script: "this.target.useClusters = !this.target.useClusters;", + script: "self.target.useClusters = !self.target.useClusters;", immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), initialize: emptyFunction }; + _saveFilterCommand = { + params: ["target"], title: "save filter", + script: "self.target._docFilters = copyField(self['target-docFilters']);", + immediate: undoBatch((source: Doc[]) => this.target._docFilters = undefined), + initialize: (button: Doc) => { button['target-docFilters'] = this.target._docFilters instanceof ObjectField && ObjectField.MakeCopy(this.target._docFilters as any as ObjectField); }, + }; - _freeform_commands = [this._viewCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; + _freeform_commands = [this._viewCommand, this._saveFilterCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; _masonry_commands = [this._contentCommand, this._templateCommand]; _schema_commands = [this._templateCommand, this._narrativeCommand]; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index e2f78d6f9..b3590ceee 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import Lightbox from 'react-image-lightbox-with-rotate'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../fields/DateField'; -import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit, AclSym, AclPrivate } from '../../../fields/Doc'; +import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit, AclSym, AclPrivate, AclAdmin } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; @@ -17,7 +17,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls } from '../../../fields/util'; +import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls, SharingPermissions } from '../../../fields/util'; import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -48,7 +48,6 @@ import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; import CollectionMenu from './CollectionMenu'; -import { SharingPermissions } from '../../util/SharingManager'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -111,7 +110,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus [AclPrivate, SharingPermissions.None], [AclReadonly, SharingPermissions.View], [AclAddonly, SharingPermissions.Add], - [AclEdit, SharingPermissions.Edit] + [AclEdit, SharingPermissions.Edit], + [AclAdmin, SharingPermissions.Admin] ]); get collectionViewType(): CollectionViewType | undefined { @@ -144,7 +144,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const effectiveAcl = GetEffectiveAcl(this.props.Document); if (added.length) { - if (effectiveAcl === AclReadonly && !getPlaygroundMode()) { + if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) { return false; } else { @@ -191,7 +191,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { - if (GetEffectiveAcl(this.props.Document) === AclEdit || getPlaygroundMode()) { + const effectiveAcl = GetEffectiveAcl(this.props.Document); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || getPlaygroundMode()) { const docs = doc instanceof Doc ? [doc] : doc as Doc[]; const targetDataDoc = this.props.Document[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7b54f1745..e22b400c0 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -945,7 +945,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { - ...this.props, + addDocument: this.props.addDocument, + removeDocument: this.props.removeDocument, + moveDocument: this.props.moveDocument, + pinToPres: this.props.pinToPres, + whenActiveChanged: this.props.whenActiveChanged, NativeHeight: returnZero, NativeWidth: returnZero, fitToBox: false, @@ -1146,7 +1150,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action componentDidMount() { super.componentDidMount?.(); - this._layoutComputeReaction = reaction(() => this.doLayoutComputation, + this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation }, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index aa9a3b4ae..764758eee 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,6 +1,6 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly } from "../../../../fields/Doc"; +import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; @@ -281,7 +281,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downX = x; this._downY = y; const effectiveAcl = GetEffectiveAcl(this.props.Document); - if (effectiveAcl === AclEdit || effectiveAcl === AclAddonly || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl) || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); this.clearSelection(); } }); diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index b17accfd6..c9d23ff3a 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -10,7 +10,7 @@ import React = require("react"); import { DocUtils } from "../../documents/Documents"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { LinkDocPreview } from "./LinkDocPreview"; -import { LinkCreatedBox } from "./LinkCreatedBox"; +import { TaskCompletionBox } from "./TaskCompletedBox"; import { LinkDescriptionPopup } from "./LinkDescriptionPopup"; import { LinkManager } from "../../util/LinkManager"; import { Tooltip } from "@material-ui/core"; @@ -99,15 +99,16 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp runInAction(() => { if (linkDoc) { - LinkCreatedBox.popupX = e.screenX; - LinkCreatedBox.popupY = e.screenY - 133; - LinkCreatedBox.linkCreated = true; + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = e.screenX; + TaskCompletionBox.popupY = e.screenY - 133; + TaskCompletionBox.taskCompleted = true; LinkDescriptionPopup.popupX = e.screenX; LinkDescriptionPopup.popupY = e.screenY - 100; LinkDescriptionPopup.descriptionPopup = true; - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); } }); @@ -132,9 +133,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp runInAction(() => { if (linkDoc) { - LinkCreatedBox.popupX = e.screenX; - LinkCreatedBox.popupY = e.screenY - 133; - LinkCreatedBox.linkCreated = true; + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = e.screenX; + TaskCompletionBox.popupY = e.screenY - 133; + TaskCompletionBox.taskCompleted = true; if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { LinkDescriptionPopup.popupX = e.screenX; @@ -142,7 +144,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp LinkDescriptionPopup.descriptionPopup = true; } - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); } }); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 998c6798e..df4576243 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,7 +3,7 @@ import * as fa from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate, AclEdit } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate, AclEdit, AclAdmin } from "../../../fields/Doc"; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; @@ -11,7 +11,7 @@ import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../fields/Types"; -import { TraceMobx, GetEffectiveAcl } from '../../../fields/util'; +import { TraceMobx, GetEffectiveAcl, SharingPermissions } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; @@ -25,7 +25,7 @@ import { InteractionUtils } from '../../util/InteractionUtils'; import { Scripting } from '../../util/Scripting'; import { SearchUtil } from '../../util/SearchUtil'; import { SelectionManager } from "../../util/SelectionManager"; -import SharingManager, { SharingPermissions } from '../../util/SharingManager'; +import SharingManager from '../../util/SharingManager'; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { CollectionView, CollectionViewType } from '../collections/CollectionView'; @@ -41,7 +41,7 @@ import { RadialMenu } from './RadialMenu'; import React = require("react"); import { DocumentLinksButton } from './DocumentLinksButton'; import { MobileInterface } from '../../../mobile/MobileInterface'; -import { LinkCreatedBox } from './LinkCreatedBox'; +import { TaskCompletionBox } from './TaskCompletedBox'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { LinkManager } from '../../util/LinkManager'; @@ -294,7 +294,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let stopPropagate = true; let preventDefault = true; !this.props.Document.isBackground && this.props.bringToFront(this.props.Document); - if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._doubleTap && this.props.renderDepth) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click if (!(e.nativeEvent as any).formattedHandled) { if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself const func = () => this.onDoubleClickHandler.script.run({ @@ -629,15 +629,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const makeLink = action((linkDoc: Doc) => { LinkManager.currentLink = linkDoc; - LinkCreatedBox.popupX = de.x; - LinkCreatedBox.popupY = de.y - 33; - LinkCreatedBox.linkCreated = true; + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = de.x; + TaskCompletionBox.popupY = de.y - 33; + TaskCompletionBox.taskCompleted = true; LinkDescriptionPopup.popupX = de.x; LinkDescriptionPopup.popupY = de.y; LinkDescriptionPopup.descriptionPopup = true; - setTimeout(action(() => LinkCreatedBox.linkCreated = false), 2500); + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); }); if (de.complete.annoDragData) { /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner @@ -757,6 +758,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const appearance = cm.findByDescription("Appearance..."); const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); + appearanceItems.push({ description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); !appearance && cm.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "compass" }); const options = cm.findByDescription("Options..."); @@ -807,7 +809,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); } - GetEffectiveAcl(this.props.Document) === AclEdit && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); + const effectiveAcl = GetEffectiveAcl(this.props.Document); + (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" }); !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); @@ -818,6 +821,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); + // to be removed for baseline + cm.addItem({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); + // const existingAcls = cm.findByDescription("Privacy..."); // const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : []; // aclItems.push({ description: "Make Add Only", event: () => this.setAcl(SharingPermissions.Add), icon: "concierge-bell" }); diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index ab34e13b0..2611d2ca7 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -67,7 +67,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( background: StrCast(refLayout._backgroundColor, StrCast(refLayout.backgroundColor)), boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined }}> - <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" /> + <FontAwesomeIcon className="fontIconBox-icon" icon={StrCast(this.dataDoc.icon, "user") as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" /> {!this.rootDoc.title ? (null) : <div className="fontIconBox-label" style={{ width: this.rootDoc.label ? "max-content" : undefined }}> {StrCast(this.rootDoc.label, StrCast(this.rootDoc.title).substring(0, 6))} </div>} </button>; return !this.layoutDoc.toolTip ? button : diff --git a/src/client/views/nodes/LinkCreatedBox.tsx b/src/client/views/nodes/LinkCreatedBox.tsx deleted file mode 100644 index 648ae23c8..000000000 --- a/src/client/views/nodes/LinkCreatedBox.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React = require("react"); -import { observer } from "mobx-react"; -import { documentSchema } from "../../../fields/documentSchemas"; -import { makeInterface } from "../../../fields/Schema"; -import "./LinkCreatedBox.scss"; -import { observable, action } from "mobx"; -import { Fade } from "@material-ui/core"; - - -@observer -export class LinkCreatedBox extends React.Component<{}> { - - @observable public static linkCreated: boolean = false; - @observable public static popupX: number = 500; - @observable public static popupY: number = 150; - - @action - public static changeLinkCreated = () => { - LinkCreatedBox.linkCreated = !LinkCreatedBox.linkCreated; - } - - render() { - return <Fade in={LinkCreatedBox.linkCreated}> - <div className="linkCreatedBox-fade" - style={{ - left: LinkCreatedBox.popupX ? LinkCreatedBox.popupX : 500, - top: LinkCreatedBox.popupY ? LinkCreatedBox.popupY : 150, - }}>Link Created</div> - </Fade>; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index 06e8d30d1..d8fe47f4e 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -4,7 +4,7 @@ import "./LinkDescriptionPopup.scss"; import { observable, action } from "mobx"; import { EditableView } from "../EditableView"; import { LinkManager } from "../../util/LinkManager"; -import { LinkCreatedBox } from "./LinkCreatedBox"; +import { TaskCompletionBox } from "./TaskCompletedBox"; @observer @@ -31,7 +31,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { onClick = (e: PointerEvent) => { if (this.popupRef && !!!this.popupRef.current?.contains(e.target as any)) { LinkDescriptionPopup.descriptionPopup = false; - LinkCreatedBox.linkCreated = false; + TaskCompletionBox.taskCompleted = false; } } diff --git a/src/client/views/nodes/LinkCreatedBox.scss b/src/client/views/nodes/TaskCompletedBox.scss index 3cbd38b55..80b750b39 100644 --- a/src/client/views/nodes/LinkCreatedBox.scss +++ b/src/client/views/nodes/TaskCompletedBox.scss @@ -1,7 +1,6 @@ -.linkCreatedBox-fade { +.taskCompletedBox-fade { border: 1px solid rgb(100, 100, 100); - width: auto; position: absolute; diff --git a/src/client/views/nodes/TaskCompletedBox.tsx b/src/client/views/nodes/TaskCompletedBox.tsx new file mode 100644 index 000000000..89602f219 --- /dev/null +++ b/src/client/views/nodes/TaskCompletedBox.tsx @@ -0,0 +1,32 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { makeInterface } from "../../../fields/Schema"; +import "./TaskCompletedBox.scss"; +import { observable, action } from "mobx"; +import { Fade } from "@material-ui/core"; + + +@observer +export class TaskCompletionBox extends React.Component<{}> { + + @observable public static taskCompleted: boolean = false; + @observable public static popupX: number = 500; + @observable public static popupY: number = 150; + @observable public static textDisplayed: string; + + @action + public static toggleTaskCompleted = () => { + TaskCompletionBox.taskCompleted = !TaskCompletionBox.taskCompleted; + } + + render() { + return <Fade in={TaskCompletionBox.taskCompleted}> + <div className="taskCompletedBox-fade" + style={{ + left: TaskCompletionBox.popupX ? TaskCompletionBox.popupX : 500, + top: TaskCompletionBox.popupY ? TaskCompletionBox.popupY : 150, + }}>{TaskCompletionBox.textDisplayed}</div> + </Fade>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 38fa66d65..6b6fc5da2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from " import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../../fields/DateField'; -import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit } from "../../../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin } from "../../../../fields/Doc"; import { documentSchema } from '../../../../fields/documentSchemas'; import applyDevTools = require("prosemirror-dev-tools"); import { removeMarkWithAttrs } from "./prosemirrorPatches"; @@ -233,7 +233,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template const json = JSON.stringify(state.toJSON()); let unchanged = true; - if (GetEffectiveAcl(this.dataDoc) === AclEdit) { + const effectiveAcl = GetEffectiveAcl(this.dataDoc); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) { this._applyingChange = true; (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))); @@ -689,7 +690,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp ); this._disposers.editorState = reaction( () => { - if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) { + if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) { return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data; } return Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data; @@ -1400,12 +1401,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp onDoubleClick={this.onDoubleClick} > <div className={`formattedTextBox-outer`} ref={this._scrollRef} - style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.isSelected() ? "none" : undefined }} + style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.active() ? "none" : undefined }} onScroll={this.onscrolled} onDrop={this.ondrop} > <div className={`formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget} style={{ padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`, - pointerEvents: !this.props.isSelected() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : "all") : undefined + pointerEvents: !this.props.active() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : undefined) : undefined }} /> </div> diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index cc90574a2..7e2175e50 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -19,9 +19,8 @@ import { DateField } from "./DateField"; import { listSpec } from "./Schema"; import { ComputedField, ScriptField } from "./ScriptField"; import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; -import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl } from "./util"; +import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions } from "./util"; import { LinkManager } from "../client/util/LinkManager"; -import { SharingPermissions } from "../client/util/SharingManager"; import JSZip = require("jszip"); import { saveAs } from "file-saver"; @@ -103,26 +102,28 @@ export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); export const AclAddonly = Symbol("AclAddonly"); export const AclEdit = Symbol("AclEdit"); +export const AclAdmin = Symbol("AclAdmin"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); -const CachedUpdates = Symbol("Cached updates"); +export const CachedUpdates = Symbol("Cached updates"); const AclMap = new Map<string, symbol>([ [SharingPermissions.None, AclPrivate], [SharingPermissions.View, AclReadonly], [SharingPermissions.Add, AclAddonly], - [SharingPermissions.Edit, AclEdit] + [SharingPermissions.Edit, AclEdit], + [SharingPermissions.Admin, AclAdmin] ]); export function fetchProto(doc: Doc) { - // if (doc.author !== Doc.CurrentUserEmail) { - untracked(() => { - const permissions: { [key: string]: symbol } = {}; + const permissions: { [key: string]: symbol } = {}; - Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!); + Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!); - if (Object.keys(permissions).length) doc[AclSym] = permissions; - }); - // } + if (Object.keys(permissions).length) doc[AclSym] = permissions; + + if (GetEffectiveAcl(doc) === AclPrivate) { + runInAction(() => doc[FieldsSym](true)); + } if (doc.proto instanceof Promise) { doc.proto.then(fetchProto); @@ -142,7 +143,7 @@ export class Doc extends RefField { has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields, ownKeys: target => { const obj = {} as any; - if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fields); + if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys); runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__); return Object.keys(obj); }, @@ -150,11 +151,11 @@ export class Doc extends RefField { if (prop.toString() === "__LAYOUT__") { return Reflect.getOwnPropertyDescriptor(target, prop); } - if (prop in target.__fields) { + if (prop in target.__fieldKeys) { return { configurable: true,//TODO Should configurable be true? enumerable: true, - value: target.__fields[prop] + value: 0//() => target.__fields[prop]) }; } return Reflect.getOwnPropertyDescriptor(target, prop); @@ -178,15 +179,23 @@ export class Doc extends RefField { this.___fields = value; for (const key in value) { const field = value[key]; + field && (this.__fieldKeys[key] = true); if (!(field instanceof ObjectField)) continue; field[Parent] = this[Self]; field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); } } + private get __fieldKeys() { return this.___fieldKeys; } + private set __fieldKeys(value) { + this.___fieldKeys = value; + } @observable private ___fields: any = {}; + @observable + private ___fieldKeys: any = {}; + private [UpdatingFromServer]: boolean = false; private [Update] = (diff: any) => { @@ -195,7 +204,14 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; - public [FieldsSym] = () => this.___fields; + public [FieldsSym] = (clear?: boolean) => { + if (clear) { + this.___fields = {}; + this.___fieldKeys = {}; + } + return this.___fields; + } + @observable public [AclSym]: { [key: string]: symbol }; public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); @@ -236,11 +252,18 @@ export class Doc extends RefField { const fKey = key.substring(7); const fn = async () => { const value = await SerializationHelper.Deserialize(set[key]); + const prev = GetEffectiveAcl(this); this[UpdatingFromServer] = true; this[fKey] = value; + if (fKey.startsWith("ACL")) { + fetchProto(this); + } this[UpdatingFromServer] = false; + if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) { + DocServer.GetRefField(this[Id], true); + } }; - if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { + if (sameAuthor || fKey.startsWith("ACL") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; await fn(); } else { @@ -893,7 +916,6 @@ export namespace Doc { return doc; } export function UnBrushDoc(doc: Doc) { - if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc; brushManager.BrushedDoc.delete(doc); brushManager.BrushedDoc.delete(Doc.GetProto(doc)); diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index 98ef3e087..c6a8d6ae9 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -31,7 +31,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu } const proto = new Proxy({}, { get(target: any, prop, receiver) { - const field = receiver.doc[prop]; + const field = receiver.doc?.[prop]; if (prop in schema) { const desc = prop === "proto" ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should? if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec diff --git a/src/fields/util.ts b/src/fields/util.ts index ef66d9633..47da7d7bf 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,15 +1,14 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, fetchProto, DataSym, DocListCast } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; import { action, trace } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; -import { SharingPermissions } from "../client/util/SharingManager"; function _readOnlySetter(): never { @@ -67,16 +66,21 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number delete curValue[Parent]; delete curValue[OnUpdate]; } + + const effectiveAcl = GetEffectiveAcl(target); + const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail); - const writeToDoc = sameAuthor || GetEffectiveAcl(target) === AclEdit || (writeMode !== DocServer.WriteMode.LiveReadonly); - const writeToServer = (sameAuthor || GetEffectiveAcl(target) === AclEdit || writeMode === DocServer.WriteMode.Default) && !playgroundMode; + const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly); + const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !playgroundMode; if (writeToDoc) { if (value === undefined) { + target.__fieldKeys && (delete target.__fieldKeys[prop]); delete target.__fields[prop]; } else { + target.__fieldKeys && (target.__fieldKeys[prop] = true); target.__fields[prop] = value; } //if (typeof value === "object" && !(value instanceof ObjectField)) debugger; @@ -126,12 +130,20 @@ export function setGroups(groups: string[]) { currentUserGroups = groups; } +export enum SharingPermissions { + Admin = "Admin", + Edit = "Can Edit", + Add = "Can Add", + View = "Can View", + None = "Not Shared" +} + export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol { - if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclEdit; + if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { - if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclEdit; + if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclAdmin; if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; @@ -142,7 +154,8 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number) [AclPrivate, 0], [AclReadonly, 1], [AclAddonly, 2], - [AclEdit, 3] + [AclEdit, 3], + [AclAdmin, 4] ]); for (const [key, value] of Object.entries(target[AclSym])) { @@ -156,7 +169,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number) } return aclPresent ? effectiveAcl : AclEdit; } - return AclEdit; + return AclAdmin; } export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) { @@ -165,7 +178,8 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc ["Not Shared", 0], ["Can View", 1], ["Can Add", 2], - ["Can Edit", 3] + ["Can Edit", 3], + ["Admin", 4] ]); const dataDoc = target[DataSym]; @@ -205,11 +219,11 @@ const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHe "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - if (GetEffectiveAcl(target, in_prop) !== AclEdit) { - return true; - } + const effectiveAcl = GetEffectiveAcl(target, in_prop); + if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true; - if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; + if (typeof prop === "string" && prop.startsWith("ACL") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; + // if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { @@ -229,6 +243,8 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; + + if (in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return undefined; if (prop === LayoutSym) { |