diff options
-rw-r--r-- | src/client/DocServer.ts | 2 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 24 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 59 | ||||
-rw-r--r-- | src/client/util/SharingManager.tsx | 64 | ||||
-rw-r--r-- | src/client/views/DocComponent.tsx | 10 | ||||
-rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 43 | ||||
-rw-r--r-- | src/client/views/collections/TreeView.tsx | 9 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/search/SearchBox.tsx | 2 | ||||
-rw-r--r-- | src/fields/Doc.ts | 13 | ||||
-rw-r--r-- | src/fields/util.ts | 23 |
11 files changed, 187 insertions, 67 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 1d7497cf8..59278d2af 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -225,7 +225,7 @@ export namespace DocServer { * the server if the document has not been cached. * @param id the id of the requested document */ - const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => { + const _GetRefFieldImpl = async (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 diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 24682cbd0..6fb6f70b3 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -648,8 +648,9 @@ export namespace Docs { * constructor just generates a new GUID. This is currently used * only when creating a DockDocument from the current user's already existing * main document. + * @param layoutData whether the fieldKey field on the layout doc should store the data or the data doc */ - function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data", protoId?: string) { + function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data", protoId?: string, layoutData?: boolean) { const viewKeys = ["x", "y", "system"]; // keys that should be addded to the view document even though they don't begin with an "_" const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, "^_"); @@ -660,7 +661,17 @@ export namespace Docs { dataProps[`${fieldKey}-lastModified`] = new DateField; dataProps["acl-Override"] = "None"; dataProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; - dataProps[fieldKey] = data; + + if (layoutData) { + viewProps[fieldKey] = data; + const list = new List<Doc>(); + console.log(DocListCast(data)); + DocListCast(data).forEach(doc => { + list.push(...DocListCast(doc.data)); + }); + dataProps[fieldKey + "-all"] = list; + } + else dataProps[fieldKey] = data; // so that the list of annotations is already initialised, prevents issues in addonly. // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. dataProps[fieldKey + "-annotations"] = new List<Doc>(); @@ -838,8 +849,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema }); } - export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id); + export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId); } export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { @@ -891,7 +902,7 @@ export namespace Docs { export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { const tabs = TreeDocument(documents, { title: "On-Screen Tabs", childDontRegisterViews: true, freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", _fitWidth: true, system: true }); const all = TreeDocument([], { title: "Off-Screen Tabs", childDontRegisterViews: true, freezeChildren: "add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", system: true }); - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List([tabs, all]), { freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List([tabs, all]), { freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id, undefined, undefined, true); } export function DirectoryImportDocument(options: DocumentOptions = {}) { @@ -1436,4 +1447,7 @@ Scripting.addGlobal(function generateLinkTitle(self: Doc) { const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null).title : "<?>"; const relation = self.linkRelationship || "to"; return `${anchor1title} (${relation}) ${anchor2title}`; +}); +Scripting.addGlobal(function openTabAlias(tab: Doc) { + CollectionDockingView.AddSplit(Doc.MakeAlias(tab), "right"); });
\ No newline at end of file diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 14bb87e89..a8327ae93 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,6 +1,6 @@ -import { computed, observable, reaction, action } from "mobx"; +import { computed, observable, reaction } from "mobx"; import * as rp from 'request-promise'; -import { DataSym, Doc, DocListCast, DocListCastAsync, AclReadonly } from "../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; @@ -34,7 +34,7 @@ import { SelectionManager } from "./SelectionManager"; import { UndoManager } from "./UndoManager"; import { SnappingManager } from "./SnappingManager"; import { InkTool } from "../../fields/InkField"; -import { computedFn } from "mobx-utils"; +import { SharingManager } from "./SharingManager"; export let resolvedPorts: { server: number, socket: number }; @@ -521,7 +521,6 @@ export class CurrentUserUtils { { title: "Import", target: Cast(doc.myImportPanel, Doc, null), icon: "upload", click: 'selectMainMenu(self)' }, { title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' }, { title: "Sharing", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc }, - // { title: "Filter", target: Cast(doc.currentFilter, Doc, null), icon: "filter", click: 'selectMainMenu(self)' }, { title: "Pres. Trails", target: Cast(doc.myPresentations, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' }, { title: "Help", target: undefined as any, icon: "question-circle", click: 'selectMainMenu(self)' }, { title: "Settings", target: undefined as any, icon: "cog", click: 'selectMainMenu(self)' }, @@ -751,7 +750,7 @@ export class CurrentUserUtils { title: "My Dashboards", _height: 400, childHideLinkButton: true, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias", treeViewTruncateTitleWidth: 150, ignoreClick: true, - _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true + _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true })); const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`); (doc.myDashboards as any as Doc).contextMenuScripts = new List<ScriptField>([newDashboard!]); @@ -852,7 +851,6 @@ export class CurrentUserUtils { CurrentUserUtils.setupPresentations(doc); CurrentUserUtils.setupFilesystem(doc); CurrentUserUtils.setupRecentlyClosedDocs(doc); - // CurrentUserUtils.setupFilterDocs(doc); CurrentUserUtils.setupUserDoc(doc); } @@ -911,9 +909,9 @@ export class CurrentUserUtils { if (doc.mySharedDocs === undefined) { let sharedDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(sharingDocumentId + "outer"); if (!sharedDocs) { - sharedDocs = Docs.Create.StackingDocument([], { - title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, - _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Add, "_acl-Public": SharingPermissions.Add, _chromeHidden: true, + sharedDocs = Docs.Create.TreeDocument([], { + title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, + _showTitle: "title", ignoreClick: false, _lockedPosition: true, "acl-Public": SharingPermissions.Add, "_acl-Public": SharingPermissions.Add, _chromeHidden: true, }, sharingDocumentId + "outer", sharingDocumentId); (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Add; } @@ -1187,15 +1185,21 @@ export class CurrentUserUtils { title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, }; const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); + const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); // add isFolder:true here? + freeformDoc.context = dashboardDoc; + DocListCast(dashboardDoc.data).forEach(doc => doc.dashboard = dashboardDoc); + DocListCast(dashboardDoc.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any; + Doc.AddDocToList(myPresentations, "data", presentation); userDoc.activePresentation = presentation; const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`); const createDashboard = ScriptField.MakeScript(`createNewDashboard()`); - dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!]); - dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard"]); + const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`); + const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`); + dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!, shareDashboard!, addToDashboards!]); + dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard", "Share Dashboard", "Add to Dashboards"]); Doc.AddDocToList(dashboards, "data", dashboardDoc); CurrentUserUtils.openDashboard(userDoc, dashboardDoc); @@ -1247,5 +1251,32 @@ Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Insta "returns all the links to the document or its annotations", "(doc: any)"); Scripting.addGlobal(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); -Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, - "toggle between regular rendeing and an informal sketch/comic style"); +Scripting.addGlobal(function shareDashboard(dashboard: Doc) { + SharingManager.Instance.open(undefined, dashboard); +}, + "opens sharing dialog for Dashboard"); +Scripting.addGlobal(function addToDashboards(dashboard: Doc) { + const dashboardAlias = Doc.MakeAlias(dashboard); + dashboardAlias.data = new List<Doc>(DocListCast(dashboard.data).map(tabFolder => Doc.MakeAlias(tabFolder))); + // DocListCast(dashboardAlias.data).forEach(tabFolder => { + // tabFolder.data = new List<Doc>(DocListCast(tabFolder.data).map(tab => Doc.MakeAlias(tab))); + // }); + Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", dashboardAlias); + CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboardAlias); +}, + "adds Dashboard to set of Dashboards"); + +Scripting.addGlobal(function dynamicOffScreenDocs(dashboard: Doc) { + if (dashboard[DataSym] instanceof Doc) { + const allDocs = DocListCast(dashboard[DataSym]["data-all"]); + console.log(allDocs); + const onScreenTab = DocListCast(dashboard.data)[0]; + const onScreenDocs = DocListCast(onScreenTab.data); + return new List<Doc>(allDocs.reduce((result: Doc[], doc) => { + !onScreenDocs.includes(doc) && (result.push(doc)); + // console.log(doc); + return result; + }, [])); + } + return []; +}); diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 5a247730e..18f254cd6 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import Select from "react-select"; import * as RequestPromise from "request-promise"; -import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { Cast, NumCast, StrCast } from "../../fields/Types"; import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util"; @@ -17,6 +17,7 @@ import { MainViewModal } from "../views/MainViewModal"; import { DocumentView } from "../views/nodes/DocumentView"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; import { SearchBox } from "../views/search/SearchBox"; +import { CurrentUserUtils } from "./CurrentUserUtils"; import { DocumentManager } from "./DocumentManager"; import { GroupManager, UserOptions } from "./GroupManager"; import { GroupMemberView } from "./GroupMemberView"; @@ -172,10 +173,14 @@ export class SharingManager extends React.Component<{}> { const target = targetDoc || this.targetDoc!; const acl = `acl-${normalizeEmail(user.email)}`; const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; + console.log(DocListCast(CurrentUserUtils.MyDashboards.data)); + console.log(target); + console.log(DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target)); + const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1; const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); return !docs.map(doc => { - doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc); + doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); if (permission === SharingPermissions.None) { if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1; @@ -184,8 +189,9 @@ export class SharingManager extends React.Component<{}> { if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1; } - distributeAcls(acl, permission as SharingPermissions, doc); + distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard); + this.setDashboardBackground(doc, permission as SharingPermissions); if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); }).some(success => !success); @@ -201,12 +207,13 @@ export class SharingManager extends React.Component<{}> { const target = targetDoc || this.targetDoc!; const key = normalizeEmail(StrCast(group.title)); const acl = `acl-${key}`; + const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1; const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); // ! ensures it returns true if document has been shared successfully, false otherwise return !docs.map(doc => { - doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc); + doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); if (permission === SharingPermissions.None) { if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1; @@ -215,7 +222,8 @@ export class SharingManager extends React.Component<{}> { if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1; } - distributeAcls(acl, permission as SharingPermissions, doc); + distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard); + this.setDashboardBackground(doc, permission as SharingPermissions); if (group instanceof Doc) { const members: string[] = JSON.parse(StrCast(group.members)); @@ -264,13 +272,34 @@ export class SharingManager extends React.Component<{}> { }); } else { + const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data); docs.forEach(doc => { - if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc); + const isDashboard = dashboards.indexOf(doc) !== -1; + if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard); }); } } /** + * Sets the background of the Dashboard if it has been shared as a visual indicator + */ + setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => { + if (Doc.IndexOf(doc, DocListCast(CurrentUserUtils.MyDashboards.data)) !== -1) { + if (permission !== SharingPermissions.None) { + doc.isShared = true; + doc.backgroundColor = "green"; + } + else { + const acls = doc[DataSym][AclSym]; + if (Object.keys(acls).every(key => key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key]))) { + doc.isShared = undefined; + doc.backgroundColor = undefined; + } + } + } + } + + /** * Removes the documents shared with a user through a group when the user is removed from the group. * @param group * @param emailId @@ -294,10 +323,11 @@ export class SharingManager extends React.Component<{}> { */ removeGroup = (group: Doc) => { if (group.docsShared) { + const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data); DocListCast(group.docsShared).forEach(doc => { const acl = `acl-${StrCast(group.title)}`; - - distributeAcls(acl, SharingPermissions.None, doc); + const isDashboard = dashboards.indexOf(doc) !== -1; + distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined, isDashboard); const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); @@ -423,16 +453,16 @@ export class SharingManager extends React.Component<{}> { } } - distributeOverCollection = (targetDoc?: Doc) => { - const target = targetDoc || this.targetDoc!; + // distributeOverCollection = (targetDoc?: Doc) => { + // const target = targetDoc || this.targetDoc!; - const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); - docs.forEach(doc => { - for (const [key, value] of Object.entries(doc[AclSym])) { - distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target); - } - }); - } + // const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); + // docs.forEach(doc => { + // for (const [key, value] of Object.entries(doc[AclSym])) { + // distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target); + // } + // }); + // } /** * Sorting algorithm to sort users. diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index a878a7afb..f1042de0f 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils'; import { List } from '../../fields/List'; import { DateField } from '../../fields/DateField'; import { ScriptField } from '../../fields/ScriptField'; -import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail } from '../../fields/util'; +import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail, inheritParentAcls } from '../../fields/util'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { DocUtils } from '../documents/Documents'; import { returnFalse } from '../../Utils'; @@ -198,15 +198,15 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) { added.forEach(d => { for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); - //else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true); - // else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d); } }); } if (effectiveAcl === AclAddonly) { added.map(doc => { + + if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc); doc.context = this.props.Document; if (annotationKey ?? this._annotationKey) Doc.GetProto(doc).annotationOn = this.props.Document; this.props.layerProvider?.(doc, true); @@ -220,6 +220,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T doc._stayInCollection = undefined; doc.context = this.props.Document; if (annotationKey ?? this._annotationKey) Doc.GetProto(doc).annotationOn = this.props.Document; + + inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc); }); const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; if (annoDocs) annoDocs.push(...added); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 388f9a909..819667834 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -4,7 +4,7 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from "mo import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import * as GoldenLayout from "../../../client/goldenLayout"; -import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, DocListCastAsync, DataSym } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -24,6 +24,7 @@ import React = require("react"); import { DocumentType } from '../../documents/DocumentTypes'; import { listSpec } from '../../../fields/Schema'; import { LightboxView } from '../LightboxView'; +import { inheritParentAcls } from '../../../fields/util'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -160,6 +161,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { } const instance = CollectionDockingView.Instance; if (!instance) return false; + else Doc.AddDocToList(instance.props.Document[DataSym], "data-all", document); const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName); if (!pullSide && stack) { @@ -381,15 +383,22 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { setTimeout(async () => { const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]); const tabs = sublists && Cast(sublists[0], Doc, null); - const other = sublists && Cast(sublists[1], Doc, null); + // const other = sublists && Cast(sublists[1], Doc, null); const tabdocs = await DocListCastAsync(tabs?.data); - const otherdocs = await DocListCastAsync(other?.data); - tabs && (Doc.GetProto(tabs).data = new List<Doc>(docs)); - const otherSet = new Set<Doc>(); - otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); - tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc)); - const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP); - other && (Doc.GetProto(other).data = new List<Doc>(vals)); + // const otherdocs = await DocListCastAsync(other?.data); + if (tabs) { + tabs.data = new List<Doc>(docs); + // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs))); + } + // const otherSet = new Set<Doc>(); + // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); + // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc)); + // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP); + // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]); + // if (other) { + // other.data = new List<Doc>(vals); + // // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals))); + // } }, 0); } @@ -399,7 +408,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); } tabCreated = (tab: any) => { - tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous abs (ie, when dragging a tab around a new tab is created for the old content) + 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) } stackCreated = (stack: any) => { @@ -407,9 +416,11 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { if (e.target === stack.header?.element[0] && e.button === 2) { const emptyPane = CurrentUserUtils.EmptyPane; emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` - }), "", stack); + const docToAdd = Docs.Create.FreeformDocument([], { + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, + }); + this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); + CollectionDockingView.AddSplit(docToAdd, "", stack); } }); @@ -430,9 +441,11 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { // stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size const emptyPane = CurrentUserUtils.EmptyPane; emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { + const docToAdd = Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` - }), "", stack); + }); + this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); + CollectionDockingView.AddSplit(docToAdd, "", stack); })); } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 2e98fb508..ba4af2a6d 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -151,7 +151,10 @@ export class TreeView extends React.Component<TreeViewProps> { this.treeViewOpen = !this.treeViewOpen; } else { // choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias + // this.props.addDocTab(CurrentUserUtils.ActiveDashboard.isShared ? Doc.MakeAlias(this.props.document) : this.props.document, "add:right"); + // choose an appropriate alias or make one -- -- choose the first alias that (1) the user owns, (2) has no context field - if I own it and someone else does not have it open,, otherwise create an alias this.props.addDocTab(this.props.document, "add:right"); + } } constructor(props: any) { @@ -507,7 +510,11 @@ export class TreeView extends React.Component<TreeViewProps> { [{ script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }]; } onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); - onChildDoubleClick = () => (!this.props.treeView.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick); + + onChildDoubleClick = () => { + console.log(this.props.document.onChildDoubleClick); + return (!this.props.treeView.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick) + }; refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document); ignoreEvent = (e: any) => { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7ec9f5f74..22bdfb1cf 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -754,16 +754,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); } - !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); - cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); - const help = cm.findByDescription("Help..."); const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" }); cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); } + if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && setTimeout(() => SelectionManager.SelectView(this.props.DocumentView(), false), 300); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 5c168d8a9..d3a023ec9 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -530,7 +530,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc <div className="searchBox-lozenge-dashboard" > <select className="searchBox-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])} value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}> - {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)} + {myDashboards.map((dash, i) => <option key={dash[Id]} value={i} style={{ backgroundColor: "black" }}> {StrCast(dash.title)} </option>)} </select> <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}> New diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index bd0ba3ad7..de4c1e5f9 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -21,8 +21,9 @@ import { listSpec } from "./Schema"; import { ComputedField, ScriptField } from "./ScriptField"; import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLField"; -import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; +import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; import JSZip = require("jszip"); +import { CurrentUserUtils } from "../client/util/CurrentUserUtils"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -423,6 +424,9 @@ export namespace Doc { return Array.from(results); } + /** + * @returns the index of doc toFind in list of docs, -1 otherwise + */ export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) { let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1); index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1); @@ -535,7 +539,7 @@ export namespace Doc { const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, rtfs, exclusions, dontCreate, asBranch))); !dontCreate && assignKey(new List<Doc>(clones)); } else if (doc[key] instanceof Doc) { - assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded teplate fields + assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields } else { !dontCreate && assignKey(ObjectField.MakeCopy(field)); if (field instanceof RichTextField) { @@ -558,7 +562,7 @@ export namespace Doc { } else if (field instanceof ObjectField) { await copyObjectField(field); } else if (field instanceof Promise) { - debugger; //This shouldn't happend... + debugger; //This shouldn't happen... } else { assignKey(field); } @@ -1147,6 +1151,9 @@ export namespace Doc { dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1; Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true); } + + if (ndoc) inheritParentAcls(CurrentUserUtils.ActiveDashboard, ndoc); + return ndoc; } export function delegateDragFactory(dragFactory: Doc) { diff --git a/src/fields/util.ts b/src/fields/util.ts index ea91cc057..882c7fee8 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -131,6 +131,17 @@ export function denormalizeEmail(email: string) { // playgroundMode = !playgroundMode; // } + +/** + * Copies parent's acl fields to the child + */ +export function inheritParentAcls(parent: Doc, child: Doc) { + const dataDoc = parent[DataSym]; + for (const key of Object.keys(dataDoc)) { + key.startsWith("acl") && distributeAcls(key, dataDoc[key], child); + } +} + /** * These are the various levels of access a user can have to a document. * @@ -215,7 +226,7 @@ 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, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) { if (!visited) visited = [] as Doc[]; if (visited.includes(target)) return; visited.push(target); @@ -236,6 +247,12 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) { target[key] = acl; layoutDocChanged = true; + + if (isDashboard) { + DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(d => { + distributeAcls(key, acl, d, inheritingFromCollection, visited); + }); + } } if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { @@ -245,12 +262,12 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc dataDocChanged = true; } - // maps over the aliases of the document + // maps over the links of the document const links = DocListCast(dataDoc.links); links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); // maps over the children of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? "-all" : "")]).map(d => { // this is now on the layoutdoc instead - figure out the "data-all" approach for the datadoc // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { distributeAcls(key, acl, d, inheritingFromCollection, visited); // } |