diff options
Diffstat (limited to 'src/client/views')
| -rw-r--r-- | src/client/views/DocComponent.tsx | 47 | ||||
| -rw-r--r-- | src/client/views/DocumentDecorations.scss | 103 | ||||
| -rw-r--r-- | src/client/views/DocumentDecorations.tsx | 114 | ||||
| -rw-r--r-- | src/client/views/Main.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/MainView.tsx | 19 | ||||
| -rw-r--r-- | src/client/views/PropertiesView.scss | 99 | ||||
| -rw-r--r-- | src/client/views/PropertiesView.tsx | 208 | ||||
| -rw-r--r-- | src/client/views/collections/CollectionDockingView.tsx | 16 | ||||
| -rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 8 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 28 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 15 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts | 20 | ||||
| -rw-r--r-- | src/client/views/topbar/TopBar.tsx | 2 |
13 files changed, 506 insertions, 175 deletions
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 70d208a0b..f3aa8451a 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,10 +1,10 @@ import { action, computed, observable } from 'mobx'; import { DateField } from '../../fields/DateField'; -import { DocListCast, Opt, Doc } from '../../fields/Doc'; +import { DocListCast, Opt, Doc, ReverseHierarchyMap, HierarchyMapping } from '../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocAcl, DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; -import { Cast, ScriptCast } from '../../fields/Types'; -import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; +import { Cast, DocCast, ScriptCast, StrCast } from '../../fields/Types'; +import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, normalizeEmail, SharingPermissions } from '../../fields/util'; import { returnFalse } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; @@ -12,6 +12,7 @@ import { InteractionUtils } from '../util/InteractionUtils'; import { UndoManager } from '../util/UndoManager'; import { DocumentView } from './nodes/DocumentView'; import { Touchable } from './Touchable'; +import { SharingManager } from '../util/SharingManager'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) export interface DocComponentProps { @@ -191,24 +192,37 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() } const added = docs; if (added.length) { - const aclKeys = Object.keys(this.props.Document[DocAcl] ?? {}); + const aclKeys = Object.keys(Doc.GetProto(this.props.Document)[DocAcl] ?? {}); + aclKeys.forEach(key => added.forEach(d => { - if (d.author === denormalizeEmail(key.substring(4)) && !d.createdFrom) { - distributeAcls(key, SharingPermissions.Admin, d); + if (key != 'acl-Me'){ + const permissionString = StrCast(Doc.GetProto(this.props.Document)[key]) + const permissionSymbol = ReverseHierarchyMap.get(permissionString)!.acl + const permission = HierarchyMapping.get(permissionSymbol)!.name + distributeAcls(key, permission, Doc.GetProto(d)) } }) ); if (effectiveAcl === AclAugment) { added.map(doc => { - if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)) && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc); doc.embedContainer = this.props.Document; if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document; Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc); + const parent = DocCast(doc.embedContainer); + doc.embedContainer && inheritParentAcls(parent, doc); + for (const key of Object.keys(parent)) { + const symbol = ReverseHierarchyMap.get(StrCast(parent[key])) + if (symbol && key.startsWith('acl')){ + const sharePermission = HierarchyMapping.get(symbol.acl!)!.name; + const user = SharingManager.Instance?.users.filter(({ user: { email } }) => normalizeEmail(email) == key.slice(4))[0]; + if (user && sharePermission !== SharingPermissions.None) return Doc.AddDocToList(user.sharingDoc, 'data', doc); + } + } }); - } else { - added + } else { + added .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) .map(doc => { // only make a pushpin if we have acl's to edit the document @@ -216,9 +230,18 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() doc._dragOnlyWithinContainer = undefined; doc.embedContainer = this.props.Document; if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; - - Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc); - }); + const parent = DocCast(doc.embedContainer); + doc.embedContainer && inheritParentAcls(parent, doc); + for (const key of Object.keys(Doc.GetProto(parent))) { + const symbol = ReverseHierarchyMap.get(StrCast(parent[key])) + if (symbol && key.startsWith('acl')){ + const sharePermission = HierarchyMapping.get(symbol.acl!)!.name; + const user = SharingManager.Instance?.users.filter(({ user: { email } }) => normalizeEmail(email) == key.slice(4))[0]; + if (user && sharePermission !== SharingPermissions.None) return Doc.AddDocToList(user.sharingDoc, 'data', doc); + } + } + }); + const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add))); else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index ccac5ffe4..ca3610cc0 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; @@ -152,6 +179,7 @@ $resizeHandler: 8px; display: flex; height: 20px; border-radius: 8px; + gap: 2px; outline: none; border: none; opacity: 0.3; @@ -186,6 +214,79 @@ $resizeHandler: 8px; } } + .documentDecorations-share { + background: none; + opacity: 1; + grid-column: 3; + pointer-events: auto; + min-width: fit-content; + text-align: center; + display: flex; + height: 21px; + opacity: 0.3; + &:hover { + opacity: 1; + } + + + .checkbox{ + display: inline; + + .checkbox-box{ + display: inline; + position: relative; + top: -2.5; + left: 35; + zoom: .7; + } + + & .checkbox-text{ + display: inline; + position: relative; + top: 1.5; + font-size: 8px; + } + } + + .documentDecorations-shareNone{ + width: calc(100% + 10px); + background: grey; + color: rgb(71, 71, 71); + border-radius: 8px; + border: 2px solid rgb(71, 71, 71); + } + .documentDecorations-shareEdit, + .documentDecorations-shareAdmin{ + width: calc(100% + 10px); + background: rgb(254, 254, 199); + color: rgb(75, 75, 5); + border-radius: 8px; + border: 2px solid rgb(75, 75, 5); + } + .documentDecorations-shareAugment{ + width: calc(100% + 10px); + background: rgb(208, 255, 208); + color:rgb(19, 80, 19); + border-radius: 8px; + border: 2px solid rgb(19, 80, 19); + + } + .documentDecorations-shareView{ + width: calc(100% + 10px); + background: rgb(213, 213, 255); + color: rgb(25, 25, 101); + border-radius: 8px; + border: 2px solid rgb(25, 25, 101); + } + .documentDecorations-shareNot-Shared{ + width: calc(100% + 10px); + background: rgb(255, 207, 207); + color: rgb(146, 58, 58); + border-radius: 8px; + border: 2px solid rgb(146, 58, 58); + } + } + .documentDecorations-centerCont { grid-column: 2; background: none; @@ -264,7 +365,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 3f71111e3..4454a3ec1 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -5,34 +5,35 @@ import { IconButton } from 'browndash-components'; import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { FaUndo } from 'react-icons/fa'; +import { Utils, aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents } from '../../Utils'; import { DateField } from '../../fields/DateField'; -import { Doc, DocListCast, Field } from '../../fields/Doc'; -import { AclAdmin, AclEdit, DocData, Height, Width } from '../../fields/DocSymbols'; +import { Doc, DocListCast, Field, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, DocData, Height, Width } from '../../fields/DocSymbols'; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, DocCast, 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 { GetEffectiveAcl, GetEffectiveLayoutAcl, normalizeEmail, SharingPermissions } from '../../fields/util'; import { DocumentType } from '../documents/DocumentTypes'; +import { Docs } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; +import { SettingsManager } from '../util/SettingsManager'; import { SnappingManager } from '../util/SnappingManager'; import { UndoManager } from '../util/UndoManager'; -import { CollectionDockingView } from './collections/CollectionDockingView'; -import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; -import { Colors } from './global/globalEnums'; -import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; +import { InkingStroke } from './InkingStroke'; import { LightboxView } from './LightboxView'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +import { CollectionFreeFormView } from './collections/collectionFreeForm'; +import { Colors } from './global/globalEnums'; import { DocumentView, OpenWhereMod } from './nodes/DocumentView'; -import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import React = require('react'); import _ = require('lodash'); @@ -64,6 +65,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @observable private _isRotating: boolean = false; @observable private _isRounding: boolean = false; @observable private _isResizing: boolean = false; + @observable private showLayoutAcl: boolean = false; constructor(props: any) { super(props); @@ -162,33 +164,46 @@ 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 effectiveLayoutAcl = GetEffectiveLayoutAcl(first.rootDoc); + if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == 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 effectiveLayoutAcl = GetEffectiveLayoutAcl(first.rootDoc); + if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == 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); @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { + const first = SelectionManager.Views()[0]; + const effectiveLayoutAcl = GetEffectiveLayoutAcl(first.rootDoc); + if (effectiveLayoutAcl != AclAdmin && effectiveLayoutAcl != AclEdit && effectiveLayoutAcl != AclAugment){ + return false; + } const dragDocView = SelectionManager.Views()[0]; const containers = new Set<Doc | undefined>(); SelectionManager.Views().forEach(v => containers.add(DocCast(v.rootDoc.embedContainer))); @@ -481,6 +496,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 = GetEffectiveLayoutAcl(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); @@ -746,9 +763,16 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P setTimeout(action(() => (this._showNothing = true))); return null; } + + // sharing + const acl = this.showLayoutAcl ? GetEffectiveLayoutAcl(seldocview.rootDoc) : GetEffectiveAcl(seldocview.rootDoc); + const docShareMode = HierarchyMapping.get(acl)!.name + const shareMode = StrCast(docShareMode); + var shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image; + // 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.layout_hideResizeHandles || this._isRounding || this._isRotating; + const hideResizers = ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveLayoutAcl(seldocview.rootDoc)) || hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating; const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.layout_hideDecorationTitle || this._isRounding || this._isRotating; const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.layout_hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button @@ -769,7 +793,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DocData]) : AclEdit; return collectionAcl !== AclAdmin && collectionAcl !== AclEdit && 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 className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => click!(e)))}> @@ -802,6 +825,27 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const radiusHandle = (borderRadius / docMax) * maxDist; const radiusHandleLocation = Math.min(radiusHandle, maxDist); + const sharingMenu = docShareMode ? ( + <div className='documentDecorations-share' > + <div className={`documentDecorations-share${shareMode}`}> + + {shareSymbolIcon + ' ' + shareMode} + + {!Doc.noviceMode ? + <div className='checkbox'> + <div className='checkbox-box'> + <input type="checkbox" checked={this.showLayoutAcl} onChange={action(() => (this.showLayoutAcl = !this.showLayoutAcl))} /> + </div> + <div className='checkbox-text'> Layout </div> + </div> + : null } + + </div> + </div> + ) : ( + <div /> + ); + const titleArea = this._editingTitle ? ( <input ref={this._keyinput} @@ -816,8 +860,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onPointerDown={e => e.stopPropagation()} /> ) : ( - <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}> - <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span> + <div className="documentDecorations-title" key="title" onPointerDown={e => {e.stopPropagation}}> + { hideTitle ? null : <span className={`documentDecorations-titleSpan${colorScheme}`} onPointerDown={this.onTitleDown}>{this.selectionTitle}</span>} + {sharingMenu} {!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()}> @@ -827,6 +872,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P )} </div> ); + return ( <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}> <div @@ -859,8 +905,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P <div className="documentDecorations-topbar" style={{ display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined }} onPointerDown={this.onContainerDown}> {hideDeleteButton ? null : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} - {hideTitle ? null : titleArea} - {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as new embedding, shift: in new collection)')} + {titleArea} + {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 b0b757388..ed39cde13 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -27,7 +27,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/MainView.tsx b/src/client/views/MainView.tsx index ab2e0f7c5..258674d53 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -157,33 +157,28 @@ export class MainView extends React.Component { 'dataTransition', 'viewTransition', 'treeViewOpen', - 'layout_showSidebar', 'carousel_index', 'itemIndex', // for changing slides in presentations 'layout_sidebarWidthPercent', 'layout_currentTimecode', 'layout_timelineHeightPercent', + 'layout_hideMinimap', + 'layout_showSidebar', + 'layout_scrollTop', + 'layout_fitWidth', + 'layout_curPage', 'presStatus', 'freeform_panX', 'freeform_panY', + 'freeform_scale', 'overlayX', 'overlayY', - 'layout_fitWidth', - 'nativeWidth', - 'nativeHeight', 'text_scrollHeight', 'text_height', - 'layout_hideMinimap', - 'freeform_scale', - 'layout_scrollTop', 'hidden', - 'layout_curPage', - 'type_collection', + //'type_collection', 'chromeHidden', 'currentFrame', - 'width', - 'height', - 'nativeWidth', ]); // can play with these fields on someone else's } DocServer.GetRefField('rtfProto').then( diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 897be9a32..f2be966b9 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -135,12 +135,12 @@ } .propertiesView-acls-checkbox { - margin-top: -20px; + margin-top: -15px; + margin-bottom: -10px; .propertiesView-acls-checkbox-text { - font-size: 7px; - margin-top: -10px; - margin-left: 6px; + display: inline; + font-size: 9px; } } } @@ -160,6 +160,59 @@ } } + .propertiesView-shareDropDown{ + margin-right: 10px; + min-width: 65px; + + & .propertiesView-shareDropDownNone{ + height: 16px; + padding: 0px; + padding-left: 3px; + background: grey; + color: rgb(71, 71, 71); + border-radius: 6px; + border: 1px solid rgb(71, 71, 71); + } + & .propertiesView-shareDropDownEdit, + .propertiesView-shareDropDownAdmin{ + height: 16px; + padding: 0px; + padding-left: 3px; + background: rgb(254, 254, 199); + color: rgb(75, 75, 5); + border-radius: 6px; + border: 1px solid rgb(75, 75, 5); + } + & .propertiesView-shareDropDownAugment{ + height: 16px; + padding: 0px; + padding-left: 3px; + background: rgb(208, 255, 208); + color:rgb(19, 80, 19); + border-radius: 6px; + border: 1px solid rgb(19, 80, 19); + + } + & .propertiesView-shareDropDownView{ + height: 16px; + padding: 0px; + padding-left: 3px; + background: rgb(213, 213, 255); + color: rgb(25, 25, 101); + border-radius: 6px; + border: 1px solid rgb(25, 25, 101); + } + & .propertiesView-shareDropDownNot-Shared{ + height: 16px; + padding: 0px; + padding-left: 3px; + background: rgb(255, 207, 207); + 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; @@ -317,13 +370,13 @@ } .expansion-button { - margin-left: -20; + margin-left: -15px; + margin-right: 20px; .expansion-button-icon { width: 11px; height: 11px; color: black; - margin-left: 27px; &:hover { color: rgb(131, 131, 131); @@ -340,18 +393,13 @@ 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%; + width: 97%; .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 { @@ -372,19 +420,9 @@ .propertiesView-sharingTable-item-permission { display: flex; align-items: flex-end; + text-align: right; margin-left: auto; - - .permissions-select { - border: none; - background-color: inherit; - width: 87px; - text-align: justify; // for Edge - text-align-last: end; - - &:hover { - cursor: pointer; - } - } + margin-right: -12px; } &:last-child { @@ -393,6 +431,17 @@ } } + .propertiesView-permissions-select { + background-color: inherit; + background: inherit; + border: none; + background: inherit; + width: max; + text-align: left; + display: flex; + right: 35px; + } + .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 09aac053a..14291b537 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -3,38 +3,39 @@ 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 { concat, intersection } from 'lodash'; +import { Lambda, action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { Doc, Field, FieldResult, HierarchyMapping, NumListCast, Opt, StrListCast } from '../../fields/Doc'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; +import { Doc, Field, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc'; import { AclAdmin, DocAcl, DocData, Height, Width } from '../../fields/DocSymbols'; 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 { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; +import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util'; import { DocumentType } from '../documents/DocumentTypes'; import { DocumentManager } from '../util/DocumentManager'; +import { GroupManager } from '../util/GroupManager'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { SharingManager } from '../util/SharingManager'; import { Transform } from '../util/Transform'; -import { undoable, undoBatch, UndoManager } from '../util/UndoManager'; +import { UndoManager, undoBatch, undoable } from '../util/UndoManager'; import { EditableView } from './EditableView'; import { FilterPanel } from './FilterPanel'; -import { Colors } from './global/globalEnums'; import { InkStrokeProperties } from './InkStrokeProperties'; -import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView'; -import { KeyValueBox } from './nodes/KeyValueBox'; -import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; import { PropertiesButtons } from './PropertiesButtons'; import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector'; import { PropertiesDocContextSelector } from './PropertiesDocContextSelector'; import './PropertiesView.scss'; import { DefaultStyleProvider } from './StyleProvider'; +import { Colors } from './global/globalEnums'; +import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView'; +import { KeyValueBox } from './nodes/KeyValueBox'; +import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -301,7 +302,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @undoBatch changePermissions = (e: any, user: string) => { const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc! : DocCast(doc)[DocData])); - SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); + SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs, this.layoutDocAcls); }; /** @@ -310,15 +311,16 @@ 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}> - {' '} - {permission}{' '} + <option className="propertiesView-permisssions-select" key={permission} value={permission}> + {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} </option> ))} </select> @@ -361,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" @@ -374,58 +379,156 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> {/* {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} + {this.colorACLDropDown(name, admin, permission, showExpansionIcon)} + {(permission === 'Owner' && name == 'Me') || showExpansionIcon ? this.expansionIcon : null} </div> </div> ); } /** + * @returns a colored dropdown bar reflective of the permission + */ + colorACLDropDown(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { + var shareImage = ReverseHierarchyMap.get(permission)?.image; + return ( + <div> + <div className={'propertiesView-shareDropDown'}> + <div className={`propertiesView-shareDropDown${permission}`}> + <div className="propertiesView-shareDropDown"> + {admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission) : concat(shareImage, ' ', permission)} + </div> + </div> + </div> + </div> + ); + } + + /** + * Sorting algorithm to sort users. + */ + sortUsers = (u1: String, u2: String) => { + return u1 > u2 ? -1 : u1 === u2 ? 0 : 1; + }; + + /** + * Sorting algorithm to sort groups. + */ + sortGroups = (group1: Doc, group2: Doc) => { + const g1 = StrCast(group1.title); + const g2 = StrCast(group2.title); + return g1 > g2 ? -1 : g1 === g2 ? 0 : 1; + }; + + /** * @returns the sharing and permissions panel. */ @computed get sharingTable() { // all selected docs const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutDocAcls ? this.selectedDoc : this.dataDoc!] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DocData])); - 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 showAdmin = GetEffectiveAcl(target) == AclAdmin + const individualTableEntries = []; + const usersAdded: string[] = []; // all shared users being added - organized by denormalized email - // users in common between all docs - const commonKeys: string[] = intersection(...docs.map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl]).filter(key => key !== 'acl-Me'))); + // adds each user to usersAdded + SharingManager.Instance.users.forEach(eachUser => { + var userOnDoc = true; + if (this.selectedDoc) { + if (this.selectedDoc['acl-' + normalizeEmail(eachUser.user.email)] == '' || this.selectedDoc['acl-' + normalizeEmail(eachUser.user.email)] == undefined) { + userOnDoc = false; + } + } + if (userOnDoc && !usersAdded.includes(eachUser.user.email) && eachUser.user.email != 'Public' && eachUser.user.email != target.author) { + usersAdded.push(eachUser.user.email); + } + }); - const tableEntries = []; + // sorts and then adds each user to the table + usersAdded.sort(this.sortUsers); + usersAdded.map(userEmail => { + const userKey = `acl-${normalizeEmail(userEmail)}`; + var permission; + if (this.layoutDocAcls){ + if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name; + else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[userKey]); + else permission = StrCast(Doc.GetProto(target)?.[userKey]); + } + else permission = StrCast(target[userKey]); + individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user + }); - // 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?.[DocAcl]?.[key] === docs[0]?.[DocAcl]?.[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[DocAcl][key])!.name : '-multiple-')); + // adds current user + var userEmail = Doc.CurrentUserEmail; + const userKey = `acl-${normalizeEmail(userEmail)}`; + if (userEmail == 'guest') userEmail = 'Public'; + if (!usersAdded.includes(userEmail) && userEmail != 'Public' && userEmail != target.author) { + var permission; + if (this.layoutDocAcls){ + if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name; + else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[userKey]); + else permission = StrCast(Doc.GetProto(target)?.[userKey]); + } + else permission = StrCast(target[userKey]); + individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user + } + + // shift owner to top + individualTableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'), false); + + // adds groups + const groupTableEntries: JSX.Element[] = []; + const groupList = GroupManager.Instance?.allGroups || []; + groupList.sort(this.sortGroups) + groupList.map(group => { + if (group.title != 'Public' && this.selectedDoc) { + const groupKey = 'acl-' + normalizeEmail(StrCast(group.title)); + if (this.selectedDoc[groupKey] != '' && this.selectedDoc[groupKey] != undefined) { + var permission; + if (this.layoutDocAcls){ + if (target[DocAcl][groupKey]){ + permission = HierarchyMapping.get(target[DocAcl][groupKey])?.name; + } + else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[groupKey]); + else permission = StrCast(Doc.GetProto(target)?.[groupKey]); + } + else permission = StrCast(target[groupKey]); + groupTableEntries.unshift(this.sharingItem(StrCast(group.title), showAdmin, permission!, false)); } } + }); + + // public permission + let publicPermission = StrCast(target['acl-Public']); + if (this.layoutDocAcls){ + if (target['acl-Public-layout']) publicPermission = StrCast(target['acl-Public-layout']); + else if (target['embedContainer']) publicPermission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))['acl-Public']); + else StrCast(Doc.GetProto(target)['acl-Public']); } - 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, StrCast(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 - ) + return ( + <div> + <br/> + Public / Guest Users + <div>{this.colorACLDropDown('Public', showAdmin, publicPermission!, false)}</div> + <div> + {' '} + <br></br> Individual Users with Access to this Document{' '} + </div> + <div className="propertiesView-sharingTable">{<div> {individualTableEntries}</div>}</div> + {groupTableEntries.length>0 ? + <div> + <div> + {' '} + <br></br> Groups with Access to this Document{' '} + </div> + <div className="propertiesView-sharingTable">{<div> {groupTableEntries}</div>}</div> + </div> + : null} + </div> ); - - return <div className="propertiesView-sharingTable">{tableEntries}</div>; } @computed get fieldsCheckbox() { @@ -920,12 +1023,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {!this.openSharing ? null : ( <div className="propertiesView-sharing-content"> <div className="propertiesView-buttonContainer"> - {!Doc.noviceMode ? ( - <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} + <div className="propertiesView-acls-checkbox"> + <div className="propertiesView-acls-checkbox-text"> Show / Contol Layout Permissions </div> + <Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> + </div> + {/* <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/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 2ed55b3ca..32fb4d8df 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -2,13 +2,13 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from 'mo import { observer } from 'mobx-react'; import * as ReactDOM from 'react-dom/client'; import * as GoldenLayout from '../../../client/goldenLayout'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, HierarchyMapping, Opt, ReverseHierarchyMap } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { inheritParentAcls } from '../../../fields/util'; +import { distributeAcls, inheritParentAcls } from '../../../fields/util'; import { emptyFunction, incrementTitleCopy } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; @@ -30,6 +30,7 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; import React = require('react'); import { DocumentManager } from '../../util/DocumentManager'; +import { DocAcl } from '../../../fields/DocSymbols'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -494,6 +495,15 @@ export class CollectionDockingView extends CollectionSubView() { } }; tabCreated = (tab: any) => { + const aclKeys = Object.keys(Doc.GetProto(this.props.Document)[DocAcl] ?? {}); + aclKeys.forEach(key => { + if (key != 'acl-Me'){ + const permissionString = StrCast(Doc.GetProto(this.props.Document)[key]) + const permissionSymbol = ReverseHierarchyMap.get(permissionString)!.acl + const permission = HierarchyMapping.get(permissionSymbol)!.name + distributeAcls(key, permission, Doc.GetProto(tab)) + } + }); this.tabMap.add(tab); tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content) }; @@ -554,7 +564,7 @@ export class CollectionDockingView extends CollectionSubView() { _freeform_backgroundGrid: true, title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); - this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); + this.props.Document.isShared && inheritParentAcls(Doc.GetProto(this.props.Document), Doc.GetProto(docToAdd)); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }) diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5febbe83e..9b0abc48b 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -9,7 +9,7 @@ import { RichTextField } from '../../../../fields/RichTextField'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { Cast, DocCast, FieldValue, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; -import { GetEffectiveAcl } from '../../../../fields/util'; +import { GetEffectiveAcl, SharingPermissions } from '../../../../fields/util'; import { intersectRect, returnFalse, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents'; @@ -390,7 +390,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque newCollection.x = this.Bounds.left; newCollection.y = this.Bounds.top; newCollection.layout_fitWidth = true; - selected.forEach(d => (d.embedContainer = newCollection)); + newCollection['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; + selected.forEach(d => { + d.embedContainer = newCollection; + d['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; + }); this.hideMarquee(); return newCollection; }); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 19f9f15a4..dab269474 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1084,8 +1084,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get innards() { TraceMobx(); const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1; - const layout_showTitle = this.layout_showTitle?.split(':')[0]; - const layout_showTitleHover = this.layout_showTitle?.includes(':hover'); + const showTitle = this.layout_showTitle?.split(':')[0]; + const showTitleHover = this.layout_showTitle?.includes(':hover'); const captionView = !this.layout_showCaption ? null : ( <div className="documentView-captionWrapper" @@ -1109,27 +1109,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps /> </div> ); - const targetDoc = layout_showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; + const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; const background = StrCast( SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.userColor, Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)' ); - const layout_sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); - const titleView = !layout_showTitle ? null : ( + const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); + const titleView = !showTitle ? null : ( <div - className={`documentView-titleWrapper${layout_showTitleHover ? '-hover' : ''}`} + className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} key="title" style={{ position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, - width: !this.headerMargin ? `calc(${layout_sidebarWidthPercent || 100}% - 18px)` : (layout_sidebarWidthPercent || 100) + '%', // leave room for annotation button + width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button color: lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, }}> <EditableView ref={this._titleRef} - contents={layout_showTitle + contents={showTitle .split(';') .map(field => field.trim()) .map(field => targetDoc[field]?.toString()) @@ -1138,7 +1138,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps fontSize={10} GetValue={() => { this.props.select(false); - return layout_showTitle.split(';').length === 1 ? layout_showTitle + '=' + Field.toString(targetDoc[layout_showTitle.split(';')[0]] as any as Field) : '#' + layout_showTitle; + return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; }} SetValue={undoBatch((input: string) => { if (input?.startsWith('#')) { @@ -1148,17 +1148,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date'; } } else { - var value = input.replace(new RegExp(layout_showTitle + '='), '') as string | number; - if (layout_showTitle !== 'title' && Number(value).toString() === value) value = Number(value); - if (layout_showTitle.includes('Date') || layout_showTitle === 'author') return true; - Doc.SetInPlace(targetDoc, layout_showTitle, value, true); + var value = input.replace(new RegExp(showTitle + '='), '') as string | number; + if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); + if (showTitle.includes('Date') || showTitle === 'author') return true; + Doc.SetInPlace(targetDoc, showTitle, value, true); } return true; })} /> </div> ); - return this.props.hideTitle || (!layout_showTitle && !this.layout_showCaption) ? ( + return this.props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( this.contents ) : ( <div className="documentView-styleWrapper"> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 44cb56d53..202a9f851 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -12,8 +12,8 @@ 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 { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, Height, UpdatingFromServer, Width } from '../../../../fields/DocSymbols'; +import { Doc, DocListCast, StrListCast, Field, Opt } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, Height, Width, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -311,7 +311,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps 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, AclSelfEdit, 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('#')) { @@ -1829,8 +1829,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case ' ': - [AclEdit, AclAdmin, AclSelfEdit].includes(GetEffectiveAcl(this.dataDoc)) && - 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) }))); + if (e.code !== 'Space') { + [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) })) + ); + } + break; } this.startUndoTypingBatch(); }; diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 8d57cc081..d2273c91c 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -4,8 +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 { Doc } from '../../../../fields/Doc'; -import { AclAugment, AclSelfEdit } from '../../../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit} from '../../../../fields/DocSymbols'; import { GetEffectiveAcl } from '../../../../fields/util'; import { Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; @@ -13,6 +12,7 @@ import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; import { OpenWhere } from '../DocumentView'; import { liftListItem, sinkListItem } from './prosemirrorPatches.js'; +import { Doc } from '../../../../fields/Doc'; const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; @@ -49,15 +49,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const canEdit = (state: any) => { 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; - } + const prevNode = state.selection.$cursor.nodeBefore; + const prevUser = prevNode.marks[prevNode.marks.length-1].attrs.userid + if (prevUser != Doc.CurrentUserEmail){ + return false; } - break; } return true; }; @@ -267,7 +263,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey bind('Backspace', (state: EditorState, dispatch: (tx: Transaction) => void) => { if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; - + if ( !deleteSelection(state, (tx: Transaction) => { dispatch(updateBullets(tx, schema)); @@ -338,7 +334,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/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 20cf563c1..9f2eafcee 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -114,7 +114,7 @@ export class TopBar extends React.Component { }} /> <Button - text={GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'View Original'} + text={GetEffectiveAcl(Doc.ActiveDashboard) === AclAdmin ? 'Share' : 'View Original'} onClick={() => { SharingManager.Instance.open(undefined, Doc.ActiveDashboard); }} |
