diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/SharingManager.scss | 15 | ||||
-rw-r--r-- | src/client/util/SharingManager.tsx | 87 | ||||
-rw-r--r-- | src/client/views/DocComponent.tsx | 49 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.scss | 80 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 117 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 2 | ||||
-rw-r--r-- | src/client/views/MarqueeAnnotator.tsx | 4 | ||||
-rw-r--r-- | src/client/views/PropertiesView.scss | 73 | ||||
-rw-r--r-- | src/client/views/PropertiesView.tsx | 143 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 17 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts | 12 | ||||
-rw-r--r-- | src/fields/Doc.ts | 10 | ||||
-rw-r--r-- | src/fields/util.ts | 51 |
13 files changed, 476 insertions, 184 deletions
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 932e94664..aa1e11ef5 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -6,7 +6,7 @@ transform: translate(-20px, -20px); } - select { + .select { text-align: justify; text-align-last: end } @@ -23,6 +23,11 @@ z-index: 999; } + .share-title { + display: inline-flex; + gap: 5px; + } + .share-container { .share-setup { display: flex; @@ -80,11 +85,11 @@ .layoutDoc-acls, .myDocs-acls { flex-direction: column; - margin-right: 12; label { font-weight: normal; font-style: italic; + padding-right: 12; } input { @@ -212,9 +217,9 @@ cursor: pointer; } - &:hover .padding { - white-space: unset; - } + // &:hover .padding { + // white-space: unset; + // } .padding { padding: 0 10px 0 0; diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 4937866f8..d7cc352b7 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -22,6 +22,7 @@ import { GroupManager, UserOptions } from './GroupManager'; import { GroupMemberView } from './GroupMemberView'; import { SelectionManager } from './SelectionManager'; import './SharingManager.scss'; +import { IconButton, Size } from 'browndash-components'; export interface User { email: string; @@ -78,6 +79,7 @@ export class SharingManager extends React.Component<{}> { @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) private populating: boolean = false; // whether the list of users is populating or not @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used + @observable private overridePrivate: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not // private get linkVisible() { @@ -156,7 +158,17 @@ export class SharingManager extends React.Component<{}> { const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1; + // setting the same acl for a docs within the doc being shared + if (this.overridePrivate) { + var childDocs = DocListCast(target.data); + childDocs.map(doc => { + this.setInternalSharing(recipient, permission, doc); + }); + } + const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); + + // ! ensures it returns true if document has been shared successfully, false otherwise return !docs .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) .map(doc => { @@ -169,7 +181,6 @@ export class SharingManager extends React.Component<{}> { } 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); @@ -188,6 +199,14 @@ export class SharingManager extends React.Component<{}> { const acl = `acl-${key}`; const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1; + // setting the same acl for a docs within the doc being shared + if (this.overridePrivate) { + var childDocs = DocListCast(target.data); + childDocs.map(doc => { + this.setInternalGroupSharing(group, permission, doc); + }); + } + const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); // ! ensures it returns true if document has been shared successfully, false otherwise @@ -258,6 +277,7 @@ export class SharingManager extends React.Component<{}> { const dashboards = DocListCast(Doc.MyDashboards.data); docs.forEach(doc => { const isDashboard = dashboards.indexOf(doc) !== -1; + if (this.overridePrivate) this.shareFromPropertiesSidebar(shareWith, permission, DocListCast(doc.data)); if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard); this.setDashboardBackground(doc, permission as SharingPermissions); }); @@ -330,22 +350,14 @@ export class SharingManager extends React.Component<{}> { // return; // } // targetDoc["acl-" + PublicKey] = permission; - // } + // }s - // private get sharingUrl() { - // if (!this.targetDoc) { - // return undefined; - // } - // const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]); - // return `${baseUrl}?sharing=true`; - // } - - // copy = action(() => { - // if (this.sharingUrl) { - // Utils.CopyText(this.sharingUrl); - // this.copied = true; - // } - // }); + /** + * Copies the Public sharing url to the user's clipboard. + */ + private copyURL = (e: any) => { + Utils.CopyText(Utils.shareUrl(this.targetDoc![Id])); + }; /** * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share @@ -354,13 +366,11 @@ export class SharingManager extends React.Component<{}> { const dropdownValues: string[] = Object.values(SharingPermissions); if (!uniform) dropdownValues.unshift('-multiple-'); if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); - return dropdownValues - .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any)) - .map(permission => ( - <option key={permission} value={permission}> - {permission} - </option> - )); + return dropdownValues.map(permission => ( + <option key={permission} value={permission}> + {permission} + </option> + )); } private focusOn = (contents: string) => { @@ -463,6 +473,7 @@ export class SharingManager extends React.Component<{}> { if (!this.targetDoc) return null; TraceMobx(); const groupList = GroupManager.Instance?.allGroups || []; + const sortedUsers = this.users .slice() .sort(this.sortUsers) @@ -504,11 +515,19 @@ export class SharingManager extends React.Component<{}> { // the list of users shared with const userListContents: (JSX.Element | null)[] = users - .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) + // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) + .filter(({ user }) => docs[0]?.author !== user.email) .map(({ user, linkDatabase, sharingDoc, userColor }) => { + const dashboardList = SelectionManager.Views().length < 2 ? [targetDoc] : SelectionManager.Views().map(docView => docView.props.Document); + // const dashboard = dashboardList[0] + const dashboard = Doc.ActiveDashboard; + var docToUse = dashboard; + + docToUse = Doc.GetProto(this.targetDoc!); + const userKey = `acl-${normalizeEmail(user.email)}`; const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).every(doc => doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey]); - const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; + const permissions = uniform ? StrCast(docToUse?.[userKey]) : '-multiple-'; return !permissions ? null : ( <div key={userKey} className={'container'}> @@ -557,7 +576,7 @@ export class SharingManager extends React.Component<{}> { const uniform = docs .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) .every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey])); - const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-'; + const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; return !permissions ? null : ( <div key={groupKey} className={'container'}> @@ -583,19 +602,16 @@ export class SharingManager extends React.Component<{}> { <div className="sharing-interface"> {GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null} <div className="sharing-contents"> - <p className={'share-title'}> + <p className="share-title"> <b>Share </b> {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} + {/* <FontAwesomeIcon title={"Copy Public URL"} icon={'copy'} size={'sm'} onClick={this.copyURL}/> */} + <IconButton size={Size.SMALL} tooltip="Copy Public URL" onClick={this.copyURL} icon={<FontAwesomeIcon icon="copy" />} /> </p> <div className={'close-button'} onClick={this.close}> <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} /> </div> - {/* {this.linkVisible ? - <div> - {this.sharingUrl} - </div> : - (null)} */} - { + {admin ? ( <div className="share-container"> <div className="share-setup"> <Select @@ -629,12 +645,15 @@ export class SharingManager extends React.Component<{}> { <div className="acl-container"> {Doc.noviceMode ? null : ( <div className="layoutDoc-acls"> + <input type="checkbox" onChange={action(() => (this.overridePrivate = !this.overridePrivate))} checked={this.overridePrivate} /> <label>Override Private </label> <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> </div> )} </div> </div> - } + ) : ( + <br></br> + )} <div className="main-container"> <div className={'individual-container'}> <div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index eba55e30c..ab97691ee 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -209,21 +209,44 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc); }); } else { - added - .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) - .map(doc => { - // only make a pushpin if we have acl's to edit the document - //DocUtils.LeavePushpin(doc); - doc._stayInCollection = undefined; - doc.context = this.props.Document; - if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; + 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); + } + }); + } - Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc); + if (effectiveAcl === AclAugment) { + added.map(doc => { + doc.context = this.props.Document; + const contextDoc = Cast(doc.context, Doc, null); + if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) { + if (contextDoc) inheritParentAcls(contextDoc, doc); + else if (Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc); + } + if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document; + Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc); }); - const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; - if (annoDocs instanceof List) annoDocs.push(...added); - else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); - targetDataDoc[(annotationKey ?? this.annotationKey) + '-lastModified'] = new DateField(new Date(Date.now())); + } else { + added + .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) + .map(doc => { + // only make a pushpin if we have acl's to edit the document + //DocUtils.LeavePushpin(doc); + doc._stayInCollection = undefined; + doc.context = this.props.Document; + if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; + + const contextDoc = Cast(doc.context, Doc, null); + if (contextDoc) inheritParentAcls(contextDoc, doc); + else if (Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc); + }); + const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; + if (annoDocs instanceof List) annoDocs.push(...added); + else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); + targetDataDoc[(annotationKey ?? this.annotationKey) + '-lastModified'] = new DateField(new Date(Date.now())); + } } } return true; diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index ccac5ffe4..32bcc872a 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -112,6 +112,33 @@ $resizeHandler: 8px; } } + .documentDecorations-lockButton { + display: flex; + align-items: center; + justify-content: center; + background: grey; + border: solid 1.5px rgb(72, 71, 71); + color: grey; + transition: 0.1s ease; + opacity: 1; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + opacity: 0.5; + cursor: pointer; + + &:hover { + color: rgb(72, 71, 71); + opacity: 1; + } + + > svg { + margin: 0; + } + } + .documentDecorations-minimizeButton { display: flex; align-items: center; @@ -186,6 +213,57 @@ $resizeHandler: 8px; } } + .documentDecorations-share { + background: none; + opacity: 1; + grid-column: 3; + pointer-events: auto; + overflow: hidden; + text-align: center; + width: 100%; + max-width: 120px; + display: flex; + height: 20px; + border-radius: 8px; + border-width: 10px; + opacity: 0.3; + &:hover { + opacity: 1; + } + .documentDecorations-shareNone, + .documentDecorations-shareAdmin{ + width: calc(100% + 10px); + background: grey; + color: rgb(71, 71, 71); + border-color: rgb(71, 71, 71); + } + .documentDecorations-shareEdit{ + width: calc(100% + 10px); + background: rgb(235, 235, 145); + color: rgb(75, 75, 5); + border-color: rgb(75, 75, 5); + } + .documentDecorations-shareAugment{ + width: calc(100% + 10px); + background: rgb(160, 230, 160); + color:rgb(19, 80, 19); + border-color:rgb(19, 80, 19); + + } + .documentDecorations-shareView{ + width: calc(100% + 10px); + background: rgb(161, 161, 238); + color: rgb(25, 25, 101); + border-color: rgb(25, 25, 101);; + } + .documentDecorations-shareNot-Shared{ + width: calc(100% + 10px); + background: rgb(210, 143, 143); + color: rgb(146, 58, 58); + border-color: rgb(146, 58, 58);; + } + } + .documentDecorations-centerCont { grid-column: 2; background: none; @@ -264,7 +342,7 @@ $resizeHandler: 8px; .documentDecorations-lock { position: relative; background: black; - color: gray; + color: rgb(145, 144, 144); height: 14; width: 14; pointer-events: all; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 85d36dbf8..9aca8c339 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -2,20 +2,22 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { IconButton } from 'browndash-components'; -import { action, computed, observable, reaction, runInAction } from 'mobx'; +import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { FaUndo } from 'react-icons/fa'; import { DateField } from '../../fields/DateField'; -import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc'; -import { Document } from '../../fields/documentSchemas'; +import { AclAdmin, AclAugment, AclEdit, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc'; import { InkField } from '../../fields/InkField'; +import { RichTextField } from '../../fields/RichTextField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../fields/Types'; import { GetEffectiveAcl } from '../../fields/util'; import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; +import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; +import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SnappingManager } from '../util/SnappingManager'; import { undoBatch, UndoManager } from '../util/UndoManager'; @@ -27,15 +29,12 @@ import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; -import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; +import { DocumentView, OpenWhereMod } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; import React = require('react'); -import { RichTextField } from '../../fields/RichTextField'; -import { LinkFollower } from '../util/LinkFollower'; import _ = require('lodash'); -import { DocumentManager } from '../util/DocumentManager'; -import { isUndefined } from 'lodash'; +import { SettingsManager } from '../util/SettingsManager'; @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> { @@ -163,27 +162,35 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P }; @action onContainerDown = (e: React.PointerEvent): void => { - setupMoveUpEvents( - this, - e, - e => this.onBackgroundMove(true, e), - e => {}, - emptyFunction - ); + const first = SelectionManager.Views()[0]; + const effectiveAcl = GetEffectiveAcl(first.rootDoc); + if (effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment) { + setupMoveUpEvents( + this, + e, + e => this.onBackgroundMove(true, e), + e => {}, + emptyFunction + ); + } }; @action onTitleDown = (e: React.PointerEvent): void => { - setupMoveUpEvents( - this, - e, - e => this.onBackgroundMove(true, e), - e => {}, - action(e => { - !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString); - this._editingTitle = true; - this._keyinput.current && setTimeout(this._keyinput.current.focus); - }) - ); + const first = SelectionManager.Views()[0]; + const effectiveAcl = GetEffectiveAcl(first.rootDoc); + if (effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment) { + setupMoveUpEvents( + this, + e, + e => this.onBackgroundMove(true, e), + e => {}, + action(e => { + !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString); + this._editingTitle = true; + this._keyinput.current && setTimeout(this._keyinput.current.focus); + }) + ); + } }; onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction); @@ -479,6 +486,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { const first = SelectionManager.Views()[0]; + const effectiveAcl = GetEffectiveAcl(first.rootDoc); + if (!(effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment)) return false; if (!first) return false; let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; var fixedAspect = Doc.NativeAspect(first.layoutDoc); @@ -744,6 +753,29 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P setTimeout(action(() => (this._showNothing = true))); return null; } + + // sharing + const docShareMode = Doc.GetProto(seldocview.rootDoc)['acl-Public']; + const shareMode = StrCast(docShareMode); + var shareSymbolIcon = null; + switch (shareMode) { + case 'Edit': + shareSymbolIcon = '⬢ '; + break; + case 'Augment': + shareSymbolIcon = '⬟ '; + break; + case 'View': + shareSymbolIcon = '♦ '; + break; + case 'Not-Shared': + shareSymbolIcon = '▲ '; + break; + default: + shareSymbolIcon = ''; + break; + } + // hide the decorations if the parent chooses to hide it or if the document itself hides it const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations; const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || this._isRounding || this._isRotating; @@ -764,10 +796,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P seldocview.props.hideDeleteButton || seldocview.rootDoc.hideDeleteButton || SelectionManager.Views().some(docView => { - const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DataSym]) : AclEdit; - return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); + // const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DataSym]) : AclEdit; + // return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); + const effectiveAcl = GetEffectiveAcl(Doc.GetProto(seldocview.rootDoc)); + return docView.rootDoc.stayInCollection || (effectiveAcl !== AclAdmin && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); }); - const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top"> <div @@ -806,7 +839,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : ''; // Radius constants - const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; + const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox; const borderRadius = numberValue(StrCast(seldocview.rootDoc.borderRounding)); const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.height) / 2); const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); @@ -829,15 +862,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ) : ( <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}> <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span> - {!useLock ? null : ( - <Tooltip key="lock" title={<div className="dash-tooltip">toggle ability to interact with document</div>} placement="top"> - <div className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}> - <FontAwesomeIcon size="sm" icon="lock" /> - </div> - </Tooltip> - )} </div> ); + const sharingMenu = docShareMode ? ( + <div className='documentDecorations-share' onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => SettingsManager.propertiesWidth =250))}> + <div className={`documentDecorations-share${shareMode}`}> + <span>{shareSymbolIcon + ' ' + shareMode}</span> + </div> + </div> + ) : ( + <div /> + ); return ( <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}> <div @@ -871,6 +906,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} {hideTitle ? null : titleArea} + {sharingMenu} + {hideOpenButton ? ( + <div /> + ) : seldocview.rootDoc._lockedPosition ? ( + topBtn('lock', 'lock', this.onLockDown, undefined, 'Toggle ability to interact with document') + ) : ( + topBtn('lock', 'unlock', this.onLockDown, undefined, 'Toggle ability to interact with document') + )} {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')} </div> {hideResizers ? null : ( diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 6b18caed0..7da9dc603 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -26,7 +26,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure root.render(<FieldLoader />); window.location.search.includes('safe') && CollectionView.SetSafeMode(true); const info = await CurrentUserUtils.loadCurrentUser(); - if (info.email === 'guest') DocServer.Control.makeReadOnly(); + // if (info.email === 'guest') DocServer.Control.makeReadOnly(); await CurrentUserUtils.loadUserDocument(info.id); setTimeout(() => { document.getElementById('root')!.addEventListener( diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 8fd2b87cc..a5638f390 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -1,6 +1,6 @@ import { action, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, Opt } from '../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, DataSym, Doc, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { NumCast } from '../../fields/Types'; @@ -209,7 +209,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean, summarize?: boolean) => { // creates annotation documents for current highlights const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]); - const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations); + const annotationDoc = [AclAugment, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations); addAsAnnotation && !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc); return (annotationDoc as Doc) ?? undefined; }; diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 897be9a32..71107a5f9 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -160,6 +160,49 @@ } } + .propertiesView-shareDropDown{ + margin-right: 10px; + width: 92%; + + & .propertiesView-shareDropDownNone, + .propertiesView-shareDropDownAdmin{ + padding: 5px; + background: grey; + color: rgb(71, 71, 71); + border-radius: 6px; + border: 1px solid rgb(71, 71, 71); + } + & .propertiesView-shareDropDownEdit{ + padding: 5px; + background: rgb(235, 235, 145); + color: rgb(75, 75, 5); + border-radius: 6px; + border: 1px solid rgb(75, 75, 5); + } + & .propertiesView-shareDropDownAugment{ + padding: 5px; + background: rgb(160, 230, 160); + color:rgb(19, 80, 19); + border-radius: 6px; + border: 1px solid rgb(19, 80, 19); + + } + & .propertiesView-shareDropDownView{ + padding: 5px; + background: rgb(161, 161, 238); + color: rgb(25, 25, 101); + border-radius: 6px; + border: 1px solid rgb(25, 25, 101); + } + & .propertiesView-shareDropDownNot-Shared{ + padding: 5px; + background: rgb(210, 143, 143); + color: rgb(138, 47, 47); + border-radius: 6px; + border: 1px solid rgb(138, 47, 47); + } + } + .propertiesView-filters { //border-bottom: 1px solid black; //padding: 8.5px; @@ -340,18 +383,14 @@ padding: 5px; // remove when adding buttons border-radius: 6px; // remove when adding buttons margin-right: 10px; // remove when adding buttons - // width: 100%; - // display: inline-table; background-color: #ececec; max-height: 130px; - overflow-y: auto; width: 92%; .propertiesView-sharingTable-item { display: flex; - // padding: 5px; padding: 3px; - align-items: center; + align-items: right; border-bottom: 0.5px solid grey; &:hover .propertiesView-sharingTable-item-name { @@ -373,18 +412,6 @@ display: flex; align-items: flex-end; margin-left: auto; - - .permissions-select { - border: none; - background-color: inherit; - width: 87px; - text-align: justify; // for Edge - text-align-last: end; - - &:hover { - cursor: pointer; - } - } } &:last-child { @@ -393,6 +420,18 @@ } } + .propertiesView-permissions-select { + background-color: inherit; + background: inherit; + border: none; + background: inherit; + text-align: justify; // for Edge + text-align-last: end; + &:hover { + cursor: pointer; + } + } + .propertiesView-fields { //border-bottom: 1px solid black; //padding: 8.5px; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 0b14c7b10..a5ac58f75 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -3,17 +3,16 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@material-ui/core'; -import { intersection } from 'lodash'; import { action, computed, Lambda, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { AclAdmin, AclSym, DataSym, Doc, Field, FieldResult, HeightSym, HierarchyMapping, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; +import { AclAdmin, DataSym, Doc, Field, FieldResult, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ComputedField } from '../../fields/ScriptField'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; -import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from '../../fields/util'; +import { GetEffectiveAcl, normalizeEmail, SharingPermissions } from '../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { DocumentManager } from '../util/DocumentManager'; @@ -311,13 +310,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { getPermissionsSelect(user: string, permission: string) { const dropdownValues: string[] = Object.values(SharingPermissions); if (permission === '-multiple-') dropdownValues.unshift(permission); - if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); + if (user !== 'Override') { + dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); + } return ( - <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> + <select className="propertiesView-permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> {dropdownValues - .filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)) + .filter(permission => !Doc.noviceMode || ![SharingPermissions.View].includes(permission as any)) .map(permission => ( - <option key={permission} value={permission}> + <option className="propertiesView-permisssions-select" key={permission} value={permission}> {' '} {permission}{' '} </option> @@ -362,6 +363,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { * @returns a row of the permissions panel */ sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { + if (name == Doc.CurrentUserEmail) { + name = 'Me'; + } return ( <div className="propertiesView-sharingTable-item" @@ -376,57 +380,115 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {/* {name !== "Me" ? this.notifyIcon : null} */} <div className="propertiesView-sharingTable-item-permission"> {admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission) : permission} - {permission === 'Owner' || showExpansionIcon ? this.expansionIcon : null} + {(permission === 'Owner' && name == 'Me') || showExpansionIcon ? this.expansionIcon : null} + </div> + </div> + ); + } + + publicACLDropDown(admin: boolean, permission: string, showExpansionIcon?: boolean) { + var dropDownText = ''; + switch (StrCast(permission)) { + case 'Edit': + dropDownText = '⬢ '; + break; + case 'Augment': + dropDownText = '⬟ '; + break; + case 'View': + dropDownText = '♦ '; + break; + case 'Not-Shared': + dropDownText = '▲ '; + break; + } + + return ( + <div> + <div className={'propertiesView-shareDropDown'}> + <div className={`propertiesView-shareDropDown${permission}`}> + <div className="propertiesView-shareDropDown"> + {' '} + {dropDownText} {admin && permission !== 'Owner' ? this.getPermissionsSelect('Public', permission) : permission} + {permission === 'Owner' || showExpansionIcon ? this.expansionIcon : null} + </div> + </div> </div> </div> ); } /** + * Sorting algorithm to sort users. + */ + sortUsers = (u1: String, u2: String) => { + return u1 > u2 ? -1 : u1 === u2 ? 0 : 1; + }; + + /** * @returns the sharing and permissions panel. */ @computed get sharingTable() { + const docToUse = this.selectedDoc; + if (!docToUse) { + return null; + } // all selected docs - const docs = - SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.selectedDoc : this.selectedDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym])); - + const docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? docToUse : docToUse?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym])); const target = docs[0]; - // tslint:disable-next-line: no-unnecessary-callback-wrapper - const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); - const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); + const effectiveAcls = GetEffectiveAcl(target); + const showAdmin = effectiveAcls == AclAdmin || docToUse!['acl-' + normalizeEmail(Doc.CurrentUserEmail)] == 'Owner'; // users in common between all docs - const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me'))); - + // const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me'))); + // const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); const tableEntries = []; - - // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { - if (commonKeys.length) { - for (const key of commonKeys) { - const name = denormalizeEmail(key.substring(4)); - const uniform = docs.every(doc => doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key]); - if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) { - tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[AclSym][key])!.name : '-multiple-')); + const usersAdded: string[] = []; // all shared users being added - organized by denormalized email + + SharingManager.Instance.users.forEach(eachUser => { + var userOnDashboard = true; + var permission = StrCast(target[`acl-${normalizeEmail(eachUser.user.email)}`]); + if (Doc.ActiveDashboard) { + if (Doc.ActiveDashboard['acl-' + normalizeEmail(eachUser.user.email)] == '' || Doc.ActiveDashboard['acl-' + normalizeEmail(eachUser.user.email)] == undefined) { + userOnDashboard = false; } } + if (userOnDashboard && !usersAdded.includes(eachUser.user.email) && eachUser.user.email != 'Public' && eachUser.user.email != target.author) { + // tableEntries.unshift(this.sharingItem(eachUser.user.email, showAdmin, permission, false)); // adds each user + usersAdded.push(eachUser.user.email); + } + }); + + usersAdded.sort(this.sortUsers); + usersAdded.map(userEmail => { + const permission = StrCast(target[`acl-${normalizeEmail(userEmail)}`]); + tableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission, false)); // adds each user + }); + + // add current user + var userEmail = Doc.CurrentUserEmail; + if (userEmail == 'guest') userEmail = 'Public'; + if (!usersAdded.includes(userEmail) && userEmail != 'Public' && userEmail != target.author) { + tableEntries.unshift(this.sharingItem(userEmail, showAdmin, StrCast(target[`acl-${normalizeEmail(userEmail)}`]), false)); // adds each user + usersAdded.push(userEmail); } - const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); - // shifts the current user, owner, public to the top of the doc. - // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); - if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); - tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === target['acl-Public']) ? target['acl-Public'] || SharingPermissions.None : '-multiple-')); - tableEntries.unshift( - this.sharingItem( - 'Me', - showAdmin, - docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-', - !ownerSame - ) - ); + // shift owner to top + tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'), false); - return <div className="propertiesView-sharingTable">{tableEntries}</div>; + return ( + <div> + {' '} + Sharing Mode + <div>{this.publicACLDropDown(showAdmin, StrCast(target['acl-Public']), false)}</div> + <div> + {' '} + <br></br> Who has access to the Dashboard?{' '} + </div> + <div className="propertiesView-sharingTable">{<div> {tableEntries}</div>}</div> + </div> + ); } @computed get fieldsCheckbox() { @@ -999,12 +1061,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {!this.openSharing ? null : ( <div className="propertiesView-sharing-content"> <div className="propertiesView-buttonContainer"> - {!Doc.noviceMode ? ( + {/* {!Doc.noviceMode ? ( // what is the layout checkbox for? <div className="propertiesView-acls-checkbox"> <Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <div className="propertiesView-acls-checkbox-text">Layout</div> </div> - ) : null} + ) : null} */} + {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> <FontAwesomeIcon icon="redo-alt" color="white" size="1x" /> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index b5aa34a29..555944438 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -12,9 +12,10 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { DateField } from '../../../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, CssSym, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, StrListCast, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, CssSym, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; +import { List } from '../../../../fields/List'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from '../../../../fields/RichTextField'; import { RichTextUtils } from '../../../../fields/RichTextUtils'; @@ -34,6 +35,7 @@ import { DragManager } from '../../../util/DragManager'; import { MakeTemplate } from '../../../util/DropConverter'; import { IsFollowLinkScript } from '../../../util/LinkFollower'; import { LinkManager } from '../../../util/LinkManager'; +import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; @@ -68,8 +70,6 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -import { RTFMarkup } from '../../../util/RTFMarkup'; -import { List } from '../../../../fields/List'; const translateGoogleApi = require('translate-google-api'); export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @@ -303,11 +303,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const curProto = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype 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()); - const effectiveAcl = GetEffectiveAcl(dataDoc); + const effectiveAcl = GetEffectiveAcl(this.rootDoc); const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"')); - if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) { + if ([AclEdit, AclAdmin, AclAugment].includes(effectiveAcl)) { const accumTags = [] as string[]; state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => { if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) { @@ -1767,7 +1767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.stopPropagation(); for (var i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); - if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) { + if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail)) { e.preventDefault(); } } @@ -1786,7 +1786,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': - [AclEdit, AclAdmin, AclSelfEdit].includes(GetEffectiveAcl(this.dataDoc)) && + if (e.code == 'Space') { + break; + } + [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.rootDoc)) && this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } this.startUndoTypingBatch(); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 4dfe07b24..c4a9b4a7e 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -4,7 +4,7 @@ import { Schema } from 'prosemirror-model'; import { splitListItem, wrapInList } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; import { liftTarget } from 'prosemirror-transform'; -import { AclAugment, AclSelfEdit, Doc } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit} from '../../../../fields/Doc'; import { GetEffectiveAcl } from '../../../../fields/util'; import { Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; @@ -49,14 +49,6 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey switch (GetEffectiveAcl(props.DataDoc)) { case AclAugment: return false; - case AclSelfEdit: - for (var i = state.selection.from; i < state.selection.to; i++) { - const marks = state.doc.resolve(i)?.marks?.(); - if (marks?.some((mark: any) => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail)) { - return false; - } - } - break; } return true; }; @@ -337,7 +329,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //Command to create a blank space bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => { - if (!canEdit(state)) return true; + if (GetEffectiveAcl(props.DataDoc)!=AclEdit && GetEffectiveAcl(props.DataDoc)!=AclAugment && GetEffectiveAcl(props.DataDoc)!=AclAdmin) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); dispatch(splitMetadata(marks, state.tr)); return false; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index b8ac8fb5d..7cc7e0d2a 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -107,7 +107,6 @@ export const AclUnset = Symbol('AclUnset'); export const AclPrivate = Symbol('AclOwnerOnly'); export const AclReadonly = Symbol('AclReadOnly'); export const AclAugment = Symbol('AclAugment'); -export const AclSelfEdit = Symbol('AclSelfEdit'); export const AclEdit = Symbol('AclEdit'); export const AclAdmin = Symbol('AclAdmin'); export const UpdatingFromServer = Symbol('UpdatingFromServer'); @@ -120,7 +119,6 @@ export enum aclLevel { unshared = 0, viewable = 1, augmentable = 2, - selfEditable = 2.5, editable = 3, admin = 4, } @@ -129,7 +127,6 @@ export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermis [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None }], [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View }], [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment}], - [AclSelfEdit, { level: aclLevel.selfEditable, name: SharingPermissions.SelfEdit }], [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit }], [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin }], [AclUnset, { level: aclLevel.unset, name: SharingPermissions.Unset }], @@ -140,7 +137,6 @@ export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> // this recursively updates all protos as well. export function updateCachedAcls(doc: Doc) { if (!doc) return; - const target = (doc as any)?.__fields ?? doc; const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {}; Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl)); @@ -1086,7 +1082,7 @@ export namespace Doc { target[targetKey] = new PrefetchProxy(templateDoc); } else { titleTarget && (Doc.GetProto(target).title = titleTarget); - const setDoc = [AclAdmin, AclEdit].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target; + const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target; setDoc[targetKey] = new PrefetchProxy(templateDoc); } } @@ -1265,7 +1261,9 @@ export namespace Doc { } // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message) export function IsBrushedDegreeUnmemoized(doc: Doc) { - if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return DocBrushStatus.unbrushed; + if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) { + return DocBrushStatus.unbrushed; + } const status = brushManager.BrushedDoc.has(doc) ? DocBrushStatus.selfBrushed : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? DocBrushStatus.protoBrushed : DocBrushStatus.unbrushed; if (status === DocBrushStatus.unbrushed) { const lastBrushed = Array.from(brushManager.BrushedDoc.keys()).lastElement(); diff --git a/src/fields/util.ts b/src/fields/util.ts index d5b55867e..2bc2bb3f8 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -9,10 +9,11 @@ import { returnZero } from '../Utils'; import CursorField from './CursorField'; import { AclAdmin, + AclAugment, AclEdit, aclLevel, AclPrivate, - AclSelfEdit, + AclReadonly, AclSym, DataSym, Doc, @@ -37,6 +38,8 @@ import { RichTextField } from './RichTextField'; import { SchemaHeaderField } from './SchemaHeaderField'; import { ComputedField } from './ScriptField'; import { ScriptCast, StrCast } from './Types'; +import { SharingManager } from '../client/util/SharingManager'; +import { PropertiesView } from '../client/views/PropertiesView'; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -82,8 +85,8 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail; - const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode !== DocServer.WriteMode.LiveReadonly; - const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && value instanceof RichTextField)) && !DocServer.Control.isReadOnly(); + const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAugment || effectiveAcl === AclAdmin || writeMode !== DocServer.WriteMode.LiveReadonly; + const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAugment || effectiveAcl === AclAdmin ) && !DocServer.Control.isReadOnly(); if (writeToDoc) { if (value === undefined) { @@ -142,13 +145,13 @@ export function denormalizeEmail(email: string) { * Copies parent's acl fields to the child */ export function inheritParentAcls(parent: Doc, child: Doc) { - return; const dataDoc = parent[DataSym]; for (const key of Object.keys(dataDoc)) { // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private. const permission = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : dataDoc[key]; key.startsWith('acl') && distributeAcls(key, permission, child); } + return; } /** @@ -170,10 +173,9 @@ export enum SharingPermissions { Unset = 'None', Admin = 'Admin', Edit = 'Edit', - SelfEdit = 'Self Edit', Augment = 'Augment', View = 'View', - None = 'Not Shared', + None = 'Not-Shared', } // return acl from cache or cache the acl and return. @@ -185,6 +187,7 @@ const getEffectiveAclCache = computedFn(function (target: any, user?: string) { * Calculates the effective access right to a document for the current user. */ export function GetEffectiveAcl(target: any, user?: string): symbol { + target = Doc.GetProto(target) if (!target) return AclPrivate; if (target[UpdatingFromServer]) return AclAdmin; return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) @@ -208,8 +211,24 @@ export function SetCachedGroups(groups: string[]) { } function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[AclSym]; - if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; - + if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName(SharingPermissions.Admin)) return AclAdmin; + if (target['acl-'+normalizeEmail(Doc.CurrentUserEmail)]){ + if (target['acl-'+normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.Admin){ + return AclAdmin + } + if (target['acl-'+normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.Edit){ + return AclEdit + } + if (target['acl-'+normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.Augment){ + return AclAugment + } + if (target['acl-'+normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.View){ + return AclReadonly + } + if (target['acl-'+normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.None){ + return AclPrivate + } + } const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group if (targetAcls && Object.keys(targetAcls).length) { let effectiveAcl = AclPrivate; @@ -228,13 +247,22 @@ function getEffectiveAcl(target: any, user?: string): symbol { //const override = targetAcls['acl-Override']; // if (override !== AclUnset && override !== undefined) effectiveAcl = override; - // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl) + return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl; } // authored documents are private until an ACL is set. const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author if (targetAuthor && targetAuthor !== userChecked) return AclPrivate; return AclAdmin; + let acl = AclPrivate + if (user){ + acl = target['acl-'+user] + } + else{ + acl = target['acl-'+normalizeEmail(Doc.CurrentUserEmail)] + } + console.log(target['acl-'+normalizeEmail(Doc.CurrentUserEmail)]) + return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(acl)!.level < aclLevel.editable ? AclEdit : acl; } /** * Recursively distributes the access right for a user across the children of a document and its annotations. @@ -250,9 +278,11 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) { target[key] = acl; + Doc.GetProto(target)[key] = acl if (target !== Doc.GetProto(target)) { //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???) updateCachedAcls(target); + } return; } @@ -263,7 +293,6 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) { target[key] = acl; layoutDocChanged = true; - if (isDashboard) { DocListCastAsync(target[Doc.LayoutFieldKey(target)]).then(docs => { docs?.forEach(d => distributeAcls(key, acl, d, inheritingFromCollection, visited)); @@ -302,7 +331,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : getPropAcl(target, prop); - if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true; + if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin ) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; |