diff options
Diffstat (limited to 'src/client/views')
28 files changed, 674 insertions, 358 deletions
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss index b8a6f6c05..4551fb4f4 100644 --- a/src/client/views/DashboardView.scss +++ b/src/client/views/DashboardView.scss @@ -1,39 +1,43 @@ -@import "./global/globalCssVariables"; - +@import './global/globalCssVariables'; .dashboard-view { - padding: 50px; - display: flex; - flex-direction: row; - width: 100%; - position: absolute; - - .left-menu { - display: flex; - justify-content: flex-start; - flex-direction: column; - width: 250px; - min-width: 250px; - } - - .all-dashboards { - display: flex; - flex-direction: row; - flex-wrap: wrap; - overflow-y: scroll; - } + padding: 50px; + display: flex; + flex-direction: row; + width: 100%; + position: absolute; + height: 100%; + width:100%; + padding-right: 0px; + overflow: auto; + + .left-menu { + display: flex; + justify-content: flex-start; + flex-direction: column; + width: 250px; + min-width: 250px; + } + + .all-dashboards { + display: flex; + flex-direction: row; + flex-wrap: wrap; + overflow-y: auto; + width: 100%; + } } .text-button { cursor: pointer; - padding: 3px 0; - &:hover { - font-weight: 500; - } - - &.selected { - font-weight: 700; - } + padding: 3px 0; + &:hover { + font-weight: 500; + } + + &.selected { + font-weight: 700; + } } .new-dashboard-button { @@ -64,41 +68,51 @@ } .dashboard-container { - border-radius: 10px; - cursor: pointer; - width: 250px; - height: 200px; - outline: solid 2px $light-gray; - display: flex; - flex-direction: column; - margin: 0 0px 30px 30px; - overflow: hidden; - - &:hover{ + border-radius: 10px; + cursor: pointer; + width: 250px; + height: 200px; + outline: solid 2px $light-gray; + display: flex; + flex-direction: column; + margin: 0 0px 30px 30px; + overflow: hidden; + + &:hover { outline: solid 2px $light-blue; - } - - .title { - margin: 10px; - font-weight: 500; - } - - img { - width: auto; - height: 80%; - } - - .info { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 0px 10px; - } - - .more { - z-index: 100; - } + } + + .title { + margin: 10px; + font-weight: 500; + } + + img { + width: auto; + height: 80%; + } + + .info { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0px 10px; + } + .dashboard-status, + .dashboard-status-shared { + font-size: 9; + left: 10%; + position: relative; + top: -5; + } + .dashboard-status-shared { + background: 'lightgreen'; + } + + .more { + z-index: 100; + } } .new-dashboard { @@ -136,4 +150,4 @@ flex-direction: row; justify-content: flex-end; } -}
\ No newline at end of file +} diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index f6d843201..b8c89d2ff 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,16 +1,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, ColorPicker, FontSize, IconButton, Size } from 'browndash-components'; +import { Button, ColorPicker, Size } from 'browndash-components'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { FaPlus } from 'react-icons/fa'; import { Doc, DocListCast, DocListCastAsync } from '../../fields/Doc'; -import { DocData } from '../../fields/DocSymbols'; +import { AclPrivate, DocAcl } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { listSpec } from '../../fields/Schema'; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, DocCast, ImageCast, StrCast } from '../../fields/Types'; +import { Cast, ImageCast, StrCast } from '../../fields/Types'; import { DocServer } from '../DocServer'; import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; import { CollectionViewType } from '../documents/DocumentTypes'; @@ -25,7 +26,6 @@ import './DashboardView.scss'; import { Colors } from './global/globalEnums'; import { MainViewModal } from './MainViewModal'; import { ButtonType } from './nodes/button/FontIconBox'; -import { FaPlus } from 'react-icons/fa'; enum DashboardGroup { MyDashboards, @@ -57,28 +57,30 @@ export class DashboardView extends React.Component { }; clickDashboard = (e: React.MouseEvent, dashboard: Doc) => { - Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard); - Doc.ActiveDashboard = dashboard; + if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) { + DashboardView.openSharedDashboard(dashboard); + } else { + Doc.ActiveDashboard = dashboard; + } Doc.ActivePage = 'dashboard'; }; - getDashboards = () => { + getDashboards = (whichGroup: DashboardGroup) => { const allDashboards = DocListCast(Doc.MyDashboards.data); - if (this.selectedDashboardGroup === DashboardGroup.MyDashboards) { + if (whichGroup === DashboardGroup.MyDashboards) { return allDashboards.filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail); - } else { - const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking); - return sharedDashboards; } + const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig); + return sharedDashboards; }; isUnviewedSharedDashboard = (dashboard: Doc): boolean => { - // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking); + // const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking); return !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard); }; getSharedDashboards = () => { - const sharedDashs = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking); + const sharedDashs = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking); return sharedDashs.filter(dashboard => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard)); }; @@ -158,24 +160,44 @@ export class DashboardView extends React.Component { <Button icon={<FaPlus />} size={Size.MEDIUM} text="New" onClick={() => this.setNewDashboardName('')} /> </div> <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)}> - My Dashboards + {'My Dashboards (' + this.getDashboards(DashboardGroup.MyDashboards).length + ')'} </div> <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.SharedDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)}> - Shared Dashboards + Shared Dashboards{' '} + <span + style={{ + background: this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'lightgreen' : 'undefined', + }}> + {'(' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'} + </span> </div> </div> <div className="all-dashboards"> - {this.getDashboards().map(dashboard => { + {this.getDashboards(this.selectedDashboardGroup).map(dashboard => { const href = ImageCast(dashboard.thumb)?.url.href; + const shared = Object.keys(dashboard[DocAcl]) + .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && !['acl-Me', 'acl-Guest'].includes(key)) + .some(key => dashboard[DocAcl][key] !== AclPrivate); return ( - <div className="dashboard-container" key={dashboard[Id]} onContextMenu={e => this.onContextMenu(dashboard, e)} onClick={e => this.clickDashboard(e, dashboard)}> + <div + className="dashboard-container" + key={dashboard[Id]} // + style={{ background: this.isUnviewedSharedDashboard(dashboard) && this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? 'lightgreen' : shared ? 'lightblue' : '' }} + onContextMenu={e => this.onContextMenu(dashboard, e)} + onClick={e => this.clickDashboard(e, dashboard)}> <img src={ href ?? 'https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU=' } /> <div className="info"> - <input style={{ border: 'unset' }} className="input" onClick={e => e.stopPropagation()} defaultValue={StrCast(dashboard.title)} onChange={e => (Doc.GetProto(dashboard).title = (e.target as any).value)} /> + <input + style={{ border: 'unset', borderRadius: '5px' }} + className="input" + onClick={e => e.stopPropagation()} + defaultValue={StrCast(dashboard.title)} + onChange={e => (Doc.GetProto(dashboard).title = (e.target as any).value)} + /> {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>} <div className="more" @@ -191,6 +213,7 @@ export class DashboardView extends React.Component { <Button size={Size.SMALL} icon={<FontAwesomeIcon color="black" size="lg" icon="bars" />} /> </div> </div> + <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div> </div> ); })} @@ -221,6 +244,11 @@ export class DashboardView extends React.Component { return CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard); } + public static openSharedDashboard = (dashboard: Doc) => { + Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard); + DashboardView.openDashboard(Doc.BestEmbedding(dashboard)); + }; + /// opens a dashboard as the ActiveDashboard (and adds the dashboard to the users list of dashboards if it's not already there). /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url) public static openDashboard = (doc: Doc | undefined, fromHistory = false) => { @@ -353,8 +381,7 @@ export class DashboardView extends React.Component { }; public static createNewDashboard = (id?: string, name?: string, background?: string) => { - const dashboards = Doc.MyDashboards; - const dashboardCount = DocListCast(dashboards.data).length + 1; + const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1; const freeformOptions: DocumentOptions = { x: 0, y: 400, @@ -369,12 +396,9 @@ export class DashboardView extends React.Component { const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row'); - // switching the tabs from the datadoc to the regular doc - const dashboardTabs = DocListCast(dashboardDoc[DocData].data); - dashboardDoc.data = new List<Doc>(dashboardTabs); dashboardDoc['pane-count'] = 1; - Doc.AddDocToList(dashboards, 'data', dashboardDoc); + Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc); DashboardView.SetupDashboardTrails(dashboardDoc); @@ -436,10 +460,6 @@ export class DashboardView extends React.Component { } } -export function AddToList(MySharedDocs: Doc, arg1: string, dash: any) { - throw new Error('Function not implemented.'); -} - ScriptingGlobals.add(function createNewDashboard() { return DashboardView.createNewDashboard(); }, 'creates a new dashboard when called'); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 70d208a0b..a41fc8ded 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,15 +1,14 @@ import { action, computed, observable } from 'mobx'; import { DateField } from '../../fields/DateField'; -import { DocListCast, Opt, Doc } from '../../fields/Doc'; +import { Doc, DocListCast, HierarchyMapping, Opt, ReverseHierarchyMap } 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, StrCast } from '../../fields/Types'; +import { distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; import { returnFalse } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { InteractionUtils } from '../util/InteractionUtils'; -import { UndoManager } from '../util/UndoManager'; import { DocumentView } from './nodes/DocumentView'; import { Touchable } from './Touchable'; @@ -183,7 +182,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() if (this.props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) { return false; } - const targetDataDoc = this.props.Document[DocData]; + const targetDataDoc = this.rootDoc[DocData]; const effectiveAcl = GetEffectiveAcl(targetDataDoc); if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { @@ -191,34 +190,14 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() } const added = docs; if (added.length) { - const aclKeys = Object.keys(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 (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); + if ([AclAugment, AclEdit, AclAdmin].includes(effectiveAcl)) { + added.forEach(doc => { + doc._dragOnlyWithinContainer = undefined; + if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; + Doc.SetContainer(doc, this.rootDoc); + inheritParentAcls(targetDataDoc, doc, true); }); - } else { - added - .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) - .map(doc => { - // only make a pushpin if we have acl's to edit the document - //DocUtils.LeavePushpin(doc); - doc._dragOnlyWithinContainer = undefined; - doc.embedContainer = this.props.Document; - if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; - - Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, 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..3522830e5 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -6,8 +6,8 @@ import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { FaUndo } from 'react-icons/fa'; 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'; @@ -64,6 +64,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 +163,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 = GetEffectiveAcl(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 = GetEffectiveAcl(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 = GetEffectiveAcl(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 +495,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,9 +762,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P setTimeout(action(() => (this._showNothing = true))); return null; } + + // sharing + const acl = GetEffectiveAcl(!this.showLayoutAcl ? Doc.GetProto(seldocview.rootDoc) : 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(GetEffectiveAcl(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,18 @@ 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 +881,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 +914,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/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 47dcdd2e4..347c40c18 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -285,13 +285,17 @@ export class KeyManager { preventDefault = false; break; case 'y': - SelectionManager.DeselectAll(); - UndoManager.Redo(); + if (Doc.ActivePage !== 'home') { + SelectionManager.DeselectAll(); + UndoManager.Redo(); + } stopPropagation = false; break; case 'z': - SelectionManager.DeselectAll(); - UndoManager.Undo(); + if (Doc.ActivePage !== 'home') { + SelectionManager.DeselectAll(); + UndoManager.Undo(); + } stopPropagation = false; break; case 'a': diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b0b757388..d4db5e3e8 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( @@ -46,7 +46,6 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure d.setTime(d.getTime() + 100 * 24 * 60 * 60 * 1000); const expires = 'expires=' + d.toUTCString(); document.cookie = `loadtime=${loading};${expires};path=/`; - new LinkManager(); new TrackMovements(); new ReplayMovements(); new PingManager(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ab2e0f7c5..2f877d74d 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -153,37 +153,33 @@ export class MainView extends React.Component { } this._sidebarContent.proto = undefined; if (!MainView.Live) { - DocServer.setPlaygroundFields([ + DocServer.setLivePlaygroundFields([ 'dataTransition', 'viewTransition', 'treeViewOpen', - 'layout_showSidebar', + 'treeViewExpandedView', '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/OverlayView.scss b/src/client/views/OverlayView.scss index 033cdf1f7..5362bf9f0 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -5,7 +5,6 @@ width: 100vw; height: 100vh; /* background-color: pink; */ - z-index: 100000; } .overlayWindow-outerDiv { @@ -55,8 +54,8 @@ } .overlayView-doc { - z-index: 9002; //so that it appears above chroma + z-index: 9002; //so that it appears above chroma position: absolute; top: 0; left: 0; -}
\ No newline at end of file +} diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index b513fe245..82d2bff56 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -50,18 +50,20 @@ export class PreviewCursor extends React.Component<{}> { PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, PreviewCursor._addDocument).then(batch.end); } else if (re.test(plain)) { const url = plain; - undoBatch(() => - PreviewCursor._addDocument( - Docs.Create.WebDocument(url, { - title: url, - _width: 500, - _height: 300, - data_useCors: true, - x: newPoint[0], - y: newPoint[1], - }) - ) - )(); + if (url.startsWith(window.location.href)) { + undoBatch(() => + PreviewCursor._addDocument( + Docs.Create.WebDocument(url, { + title: url, + _width: 500, + _height: 300, + data_useCors: true, + x: newPoint[0], + y: newPoint[1], + }) + ) + )(); + } else alert('cannot paste dash into itself'); } else if (plain.startsWith('__DashDocId(') || plain.startsWith('__DashCloneId(')) { const clone = plain.startsWith('__DashCloneId('); const docids = plain.split(':'); 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..2e10bf346 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,26 +302,22 @@ 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); }; /** * @returns the options for the permissions dropdown. */ - getPermissionsSelect(user: string, permission: string) { - const dropdownValues: string[] = Object.values(SharingPermissions); + getPermissionsSelect(user: string, permission: string, showGuestOptions: boolean) { + const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); if (permission === '-multiple-') dropdownValues.unshift(permission); - if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); return ( - <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> - {dropdownValues - .filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)) - .map(permission => ( - <option key={permission} value={permission}> - {' '} - {permission}{' '} - </option> - ))} + <select className="propertiesView-permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> + {dropdownValues.map(permission => ( + <option className="propertiesView-permisssions-select" key={permission} value={permission}> + {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} + </option> + ))} </select> ); } @@ -361,6 +358,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 +374,141 @@ 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, false)} + {(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, showGuestOptions: 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, showGuestOptions) : 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 docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.selectedDoc] : SelectionManager.Views().map(docView => docView.rootDoc); 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 + + const seldoc = this.layoutDocAcls || !this.selectedDoc ? this.selectedDoc : Doc.GetProto(this.selectedDoc); + // adds each user to usersAdded + SharingManager.Instance.users.forEach(eachUser => { + var userOnDoc = true; + if (seldoc) { + if (Doc.GetT(seldoc, 'acl-' + normalizeEmail(eachUser.user.email), 'string', true) === '' || Doc.GetT(seldoc, 'acl-' + normalizeEmail(eachUser.user.email), 'string', true) === undefined) { + userOnDoc = false; + } + } + if (userOnDoc && !usersAdded.includes(eachUser.user.email) && eachUser.user.email !== 'guest' && eachUser.user.email != target.author) { + usersAdded.push(eachUser.user.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'))); + // sorts and then adds each user to the table + usersAdded.sort(this.sortUsers); + usersAdded.map(userEmail => { + const userKey = `acl-${normalizeEmail(userEmail)}`; + var aclField = Doc.GetT(this.layoutDocAcls ? target : Doc.GetProto(target), userKey, 'string', true); + var permission = StrCast(aclField); + individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user + }); - const tableEntries = []; + // adds current user + var userEmail = Doc.CurrentUserEmail; + if (userEmail == 'guest') userEmail = 'Guest'; + const userKey = `acl-${normalizeEmail(userEmail)}`; + if (!usersAdded.includes(userEmail) && userEmail !== 'Guest' && 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 + } - // 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 != 'Guest' && 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)); } } - } + }); - 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 - ) - ); + // public permission + const publicPermission = StrCast((this.layoutDocAcls ? target : Doc.GetProto(target))['acl-Guest']); - return <div className="propertiesView-sharingTable">{tableEntries}</div>; + return ( + <div> + <br /> + <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} + Guest + <div>{this.colorACLDropDown('Guest', true, publicPermission!, true)}</div> + <div> + {' '} + <br></br> Individual Users with Access to this Document{' '} + </div> + </div> + ); } @computed get fieldsCheckbox() { @@ -920,12 +1003,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/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 5dfe10def..2bc2d5e6b 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { DocumentManager } from '../util/DocumentManager'; import { CompileScript, Transformer, ts } from '../util/Scripting'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; +import { undoable } from '../util/UndoManager'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; @@ -161,7 +162,7 @@ export class ScriptingRepl extends React.Component { this.commands.push({ command: this.commandString, result: script.errors }); return; } - const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })); + const result = undoable(() => script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })), 'run:' + this.commandString)(); if (result.success) { this.commands.push({ command: this.commandString, result: result.result }); this.commandsHistory.push(this.commandString); diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 9ba0e5e26..f3471c350 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -280,7 +280,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps if (props?.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform) { return doc?.pointerEvents !== 'none' ? null : ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> - <FontAwesomeIcon icon={'lock'} style={{ color: 'red' }} size="lg" /> + <FontAwesomeIcon icon='lock' style={{ color: 'red' }} size="lg" /> </div> ); } diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 2ed55b3ca..8d1b46ebb 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -8,7 +8,7 @@ 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 { GetEffectiveAcl, inheritParentAcls } from '../../../fields/util'; import { emptyFunction, incrementTitleCopy } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; @@ -28,8 +28,9 @@ import './CollectionDockingView.scss'; import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; -import React = require('react'); import { DocumentManager } from '../../util/DocumentManager'; +import { AclAdmin, AclEdit } from '../../../fields/DocSymbols'; +import React = require('react'); const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -385,8 +386,13 @@ export class CollectionDockingView extends CollectionSubView() { .map(f => f as Doc); const changesMade = this.props.Document.dockingConfig !== json; if (changesMade) { - this.props.Document.dockingConfig = json; - this.props.Document.data = new List<Doc>(docs); + if (![AclAdmin, AclEdit].includes(GetEffectiveAcl(this.dataDoc))) { + this.layoutDoc.dockingConfig = json; + this.layoutDoc.data = new List<Doc>(docs); + } else { + Doc.SetInPlace(this.rootDoc, 'dockingConfig', json, true); + Doc.SetInPlace(this.rootDoc, 'data', new List<Doc>(docs), true); + } } this._flush?.end(); this._flush = undefined; @@ -460,7 +466,7 @@ export class CollectionDockingView extends CollectionSubView() { const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeEmbedding(origtabdoc)); if (newtabdocs.length) { Doc.GetProto(newtab).data = new List<Doc>(newtabdocs); - newtabdocs.forEach(ntab => (ntab.embedContainer = newtab)); + newtabdocs.forEach(ntab => Doc.SetContainer(ntab, newtab)); } json = json.replace(origtab[Id], newtab[Id]); return newtab; @@ -511,7 +517,7 @@ export class CollectionDockingView extends CollectionSubView() { _layout_fitWidth: true, title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); - this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); + inheritParentAcls(this.rootDoc, docToAdd, false); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }); @@ -554,7 +560,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); + inheritParentAcls(this.dataDoc, docToAdd, false); CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }) diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 4cd3885f5..d78a0e781 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -180,7 +180,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree const doAddDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => { const res = flg && Doc.AddDocToList(this.doc[DocData], this.props.fieldKey, doc, relativeTo, before); - res && (doc.embedContainer = this.props.Document); + res && Doc.SetContainer(doc, this.props.Document); return res; }, true); if (this.doc.resolvedDataDoc instanceof Promise) return false; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 4d780f46b..3473eee18 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -262,7 +262,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom; pinDoc.presDuration = pinDoc.presDuration ?? 1000; pinDoc.groupWithUp = false; - pinDoc.embedContainer = curPres; + Doc.SetContainer(pinDoc, curPres); // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area pinDoc.treeViewHeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling. diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 8d8d895c6..91d1ff11e 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -221,12 +221,10 @@ export class TreeView extends React.Component<TreeViewProps> { this.treeViewOpen = !this.treeViewOpen; } else { // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns, (2) has no context field ... otherwise make a new embedding - const bestEmbedding = - docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.props.Document) - ? docView.props.Document - : DocListCast(this.props.document.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail); - const nextBestEmbedding = DocListCast(this.props.document.proto_embeddings).find(doc => doc.author === Doc.CurrentUserEmail); - this.props.addDocTab(bestEmbedding ?? nextBestEmbedding ?? Doc.MakeEmbedding(this.props.document), OpenWhere.lightbox); + const bestEmbedding = docView.rootDoc.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.props.Document) + ? docView.rootDoc + : Doc.BestEmbedding(docView.rootDoc); + this.props.addDocTab(bestEmbedding, OpenWhere.lightbox); } }; @@ -392,7 +390,7 @@ export class TreeView extends React.Component<TreeViewProps> { const innerAdd = (doc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); - dataIsComputed && (doc.embedContainer = this.doc.embedContainer); + dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); @@ -455,7 +453,7 @@ export class TreeView extends React.Component<TreeViewProps> { const innerAdd = (doc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - dataIsComputed && (doc.embedContainer = this.doc.embedContainer); + dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); @@ -563,7 +561,7 @@ export class TreeView extends React.Component<TreeViewProps> { } const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - !dataIsComputed && added && (doc.embedContainer = this.doc.embedContainer); + !dataIsComputed && added && Doc.SetContainer(doc, DocCast(this.doc.embedContainer)); return added; }; @@ -1192,7 +1190,7 @@ export class TreeView extends React.Component<TreeViewProps> { TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined; Doc.AddDocToList(newParent, fieldKey, child, addAfter, false); newParent.treeViewOpen = true; - child.embedContainer = treeView.Document; + Doc.SetContainer(child, treeView.Document); } }; const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1])); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index fc737c4b9..079b94428 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 { distributeAcls, GetEffectiveAcl, SharingPermissions } from '../../../../fields/util'; import { intersectRect, returnFalse, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents'; @@ -380,7 +380,7 @@ 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)); + selected.forEach(d => Doc.SetContainer(d, newCollection)); this.hideMarquee(); return newCollection; }); diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 2290e0711..ca5ec9389 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -46,7 +46,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl if (dropEvent.complete.docDragData) { const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocs, this.rootDoc, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey)); - droppedDocs.lastElement().embedContainer = this.dataDoc; + Doc.SetContainer(droppedDocs.lastElement(), this.dataDoc); !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place return added; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 19f9f15a4..a4f5eb62b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -535,7 +535,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // if this is part of a template, let the event go up to the template root unless right/ctrl clicking if ( // prettier-ignore - this.props.isDocumentActive?.() && + (this.props.isDocumentActive?.() || this.props.isContentActive?.()) && !this.props.onBrowseClick?.() && !this.Document.ignoreClick && e.button === 0 && @@ -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/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 8d45c5724..a77e4bdd1 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -26,7 +26,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { this.props.select(false); this._ref.current!.mathField.focus(); - this._ref.current!.mathField.select(); + this.rootDoc.text === 'x' && this._ref.current!.mathField.select(); + EquationBox.SelectOnLoad = ''; } reaction( () => StrCast(this.dataDoc.text), @@ -35,6 +36,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { this._ref.current!.mathField.latex(text); } } + //{ fireImmediately: true } ); reaction( () => this.props.isSelected(), @@ -53,9 +55,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', '')); const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace('px', '')); if (e.key === 'Enter') { - const nextEq = Docs.Create.EquationDocument({ + const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : 'x', { title: '# math', - text: StrCast(this.dataDoc.text), _width, _height: 25, x: NumCast(this.layoutDoc.x), diff --git a/src/client/views/nodes/button/FontIconBadge.scss b/src/client/views/nodes/button/FontIconBadge.scss index 78f506e57..2ff5c651f 100644 --- a/src/client/views/nodes/button/FontIconBadge.scss +++ b/src/client/views/nodes/button/FontIconBadge.scss @@ -1,11 +1,12 @@ .fontIconBadge { - background: red; + background: lightgreen; width: 15px; height: 15px; top: 8px; + color: black; display: block; position: absolute; right: 5; border-radius: 50%; text-align: center; -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 5bba51ec8..a0b0caf98 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -597,7 +597,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b if (contentFrameNumber !== undefined) { CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color }); } else { - dv.rootDoc['_' + fieldKey] = color; + dv.dataDoc[fieldKey] = color; } }); } else { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 44cb56d53..a0eb328a1 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'; @@ -34,7 +34,6 @@ import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { MakeTemplate } from '../../../util/DropConverter'; -import { IsFollowLinkScript } from '../../../util/LinkFollower'; import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -305,13 +304,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const newText = state.doc.textBetween(0, state.doc.content.size, ' \n'); const newJson = JSON.stringify(state.toJSON()); const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box - const prevLayoutData = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template + const templateData = this.rootDoc !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const effectiveAcl = GetEffectiveAcl(dataDoc); - const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"')); + const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, ''); - 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('#')) { @@ -325,12 +324,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._applyingChange = this.fieldKey; const textChange = newText !== prevData?.Text; textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); - if ((!prevData && !protoData) || newText || (!newText && !protoData)) { + if ((!prevData && !protoData) || newText || (!newText && !templateData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (removeSelection(newJson) !== removeSelection(prevLayoutData?.Data)) { + if (this.props.isContentActive() && removeSelection(newJson) !== removeSelection(prevData?.Data)) { const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); - dataDoc[this.fieldKey + '_noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited + dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); unchanged = false; } @@ -415,6 +414,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); + this.prepareForTyping(); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); }; @@ -564,7 +564,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } if (added) { draggedDoc._freeform_fitContentsToBox = true; - draggedDoc.embedContainer = this.rootDoc; + Doc.SetContainer(draggedDoc, this.rootDoc); const view = this._editorView!; view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); } @@ -1230,6 +1230,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._disposers.selected = reaction( () => this.props.isSelected(), action(selected => { + selected && this.prepareForTyping(); if (FormattedTextBox._globalHighlights.has('Bold Text')) { this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } @@ -1524,24 +1525,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } selectOnLoad && this._editorView!.focus(); - // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. + if (this.props.isContentActive()) this.prepareForTyping(); if (this._editorView) { const tr = this._editorView.state.tr; const { from, to } = tr.selection; // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated. if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250); - this._editorView.dispatch( - this._editorView.state.tr.setStoredMarks([ - ...(this._editorView.state.storedMarks ?? []), - ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], - ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), - ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), - ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), - ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), - ]) - ); + if (FormattedTextBox.PasteOnLoad) { const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); FormattedTextBox.PasteOnLoad = undefined; @@ -1551,6 +1541,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBox.DontSelectInitialText = false; } + // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. + prepareForTyping = () => { + this._editorView?.dispatch( + this._editorView?.state.tr.setStoredMarks([ + ...(this._editorView.state.storedMarks?.filter(mark => ![schema.marks.em, schema.marks.underline, schema.marks.pFontFamily, schema.marks.pFontSize, schema.marks.strong, schema.marks.pFontColor].includes(mark.type)) ?? []), + ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], + ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), + ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), + ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), + ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), + ]) + ); + }; + componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); @@ -1829,8 +1835,11 @@ 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).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..112a0d87e 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 ? Doc.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid; + if (prevUser != Doc.CurrentUserEmail) { + return false; } - break; } return true; }; @@ -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/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index d13c09443..ab9eebd78 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -222,7 +222,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { 'width', 'layout_autoHeight', 'acl-Override', - 'acl-Public', + 'acl-Guest', 'embedContainer', 'zIndex', 'height', diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 20cf563c1..0a0bac998 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -5,7 +5,7 @@ import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaBug, FaCamera, FaStamp } from 'react-icons/fa'; -import { Doc } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { AclAdmin } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; @@ -59,7 +59,11 @@ export class TopBar extends React.Component { return ( <div className="topbar-left"> {Doc.ActiveDashboard ? ( - <IconButton onClick={this.navigateToHome} icon={<FontAwesomeIcon icon="home" />} color={this.textColor} /> + <IconButton + onClick={this.navigateToHome} + icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs.data_dashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'portrait' : 'home'} />} + color={this.textColor} + /> ) : ( <div className="logo-container"> <img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo"></img> @@ -114,7 +118,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); }} |
