diff options
Diffstat (limited to 'src/client/views')
| -rw-r--r-- | src/client/views/DocComponent.tsx | 14 | ||||
| -rw-r--r-- | src/client/views/DocumentDecorations.scss | 83 | ||||
| -rw-r--r-- | src/client/views/DocumentDecorations.tsx | 107 | ||||
| -rw-r--r-- | src/client/views/Main.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/PropertiesView.scss | 91 | ||||
| -rw-r--r-- | src/client/views/PropertiesView.tsx | 172 | ||||
| -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 | 23 | ||||
| -rw-r--r-- | src/client/views/topbar/TopBar.tsx | 2 |
10 files changed, 398 insertions, 139 deletions
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index db24229dc..7e8946ca0 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,9 +1,9 @@ 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 { Cast, ScriptCast, StrCast } from '../../fields/Types'; import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; import { returnFalse } from '../../Utils'; import { DocUtils } from '../documents/Documents'; @@ -191,11 +191,15 @@ 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, d) } }) ); diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index ccac5ffe4..0a690f751 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,59 @@ $resizeHandler: 8px; } } + .documentDecorations-share { + background: none; + opacity: 1; + grid-column: 3; + pointer-events: auto; + min-width: fit-content; + text-align: center; + width: 125px; + display: flex; + height: 21px; + opacity: 0.3; + &:hover { + opacity: 1; + } + .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 +345,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 528b82e3e..dcfaecbc1 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, 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'); @@ -162,34 +163,48 @@ 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); + console.log(effectiveAcl) + 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); @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { - const dragDocView = SelectionManager.Views()[0]; + const first = SelectionManager.Views()[0]; + const effectiveAcl = GetEffectiveAcl(first.rootDoc); + if (effectiveAcl != AclAdmin && effectiveAcl != AclEdit && effectiveAcl != AclAugment){ + return false; + } +const dragDocView = SelectionManager.Views()[0]; const containers = new Set<Doc | undefined>(); SelectionManager.Views().forEach(v => containers.add(DocCast(v.rootDoc.embedContainer))); if (containers.size > 1) return false; @@ -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 = 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); @@ -746,6 +763,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P setTimeout(action(() => (this._showNothing = true))); return null; } + + // sharing + const docShareMode = HierarchyMapping.get(GetEffectiveAcl(seldocview.rootDoc))!.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; @@ -769,7 +792,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 (docView.rootDoc.stayInCollection && !docView.rootDoc._isTimelineLabel) || (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 +824,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' + onPointerDown={e => + setupMoveUpEvents( + this, + e, + e => this.onBackgroundMove(true, e), + returnFalse, + action(() => SettingsManager.propertiesWidth =250) + ) + }> + <div className={`documentDecorations-share${shareMode}`}> + + <span>{shareSymbolIcon + ' ' + shareMode}</span> + + </div> + </div> + ) : ( + <div /> + ); + const titleArea = this._editingTitle ? ( <input ref={this._keyinput} @@ -818,6 +861,7 @@ 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> + {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 +871,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P )} </div> ); + return ( <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}> <div @@ -860,7 +905,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P {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)')} + {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/PropertiesView.scss b/src/client/views/PropertiesView.scss index 897be9a32..eafe752be 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -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 1f2e21dd5..825a802cd 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, 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; @@ -311,15 +312,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> @@ -362,6 +364,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" @@ -375,58 +380,126 @@ 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(this.selectedDoc!) == 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); + } + }); + + // sorts and then adds each user to the table + usersAdded.sort(this.sortUsers); + usersAdded.map(userEmail => { + const permission = StrCast(target[`acl-${normalizeEmail(userEmail)}`]); + individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission, false)); // adds each user + }); - const tableEntries = []; + // adds current user + var userEmail = Doc.CurrentUserEmail; + if (userEmail == 'guest') userEmail = 'Public'; + if (!usersAdded.includes(userEmail) && userEmail != 'Public' && userEmail != target.author) { + individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, StrCast(target[`acl-${normalizeEmail(userEmail)}`]), false)); // adds each user + usersAdded.push(userEmail); + } - // 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-')); + // 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) { + const permission = StrCast(target[groupKey]); + groupTableEntries.unshift(this.sharingItem(StrCast(group.title), showAdmin, permission, false)); } } - } + }); - 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> + {' '} + Public / Guest Users + <div>{this.colorACLDropDown('Public', showAdmin, StrCast(target['acl-Public']), 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() { @@ -921,12 +994,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/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 23148ed34..47f3fe37b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1058,8 +1058,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" @@ -1083,27 +1083,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(users => users.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()) @@ -1112,7 +1112,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('#')) { @@ -1122,17 +1122,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 079129dc9..dc2fa8ee8 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('#')) { @@ -1814,8 +1814,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..ec56c043b 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,16 @@ 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 content = state.selection.$anchor.path[0].content.content; + var line = content[content.length-1].content.content; + var lastEdit = line[line.length-1]; + if (line == undefined || line.length == 0){ + lastEdit = content[content.length-1]; + } + const lastEditor = lastEdit.marks[lastEdit.marks.length-1].attrs.userid + if (lastEditor != Doc.CurrentUserEmail){ + return false; } - break; } return true; }; @@ -338,7 +339,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 71daad1a9..834156295 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -103,7 +103,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); }} |
