aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/client/DocServer.ts24
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts64
-rw-r--r--src/client/util/CurrentUserUtils.ts18
-rw-r--r--src/client/util/DocumentManager.ts2
-rw-r--r--src/client/util/GroupManager.scss6
-rw-r--r--src/client/util/LinkFollower.ts2
-rw-r--r--src/client/util/LinkManager.ts65
-rw-r--r--src/client/util/RTFMarkup.tsx2
-rw-r--r--src/client/util/ScriptManager.ts38
-rw-r--r--src/client/util/SharingManager.scss162
-rw-r--r--src/client/util/SharingManager.tsx390
-rw-r--r--src/client/util/UndoManager.ts9
-rw-r--r--src/client/views/DashboardView.scss90
-rw-r--r--src/client/views/DashboardView.tsx60
-rw-r--r--src/client/views/DocComponent.tsx43
-rw-r--r--src/client/views/DocumentDecorations.scss103
-rw-r--r--src/client/views/DocumentDecorations.tsx107
-rw-r--r--src/client/views/GlobalKeyHandler.ts12
-rw-r--r--src/client/views/Main.tsx3
-rw-r--r--src/client/views/MainView.tsx22
-rw-r--r--src/client/views/OverlayView.scss5
-rw-r--r--src/client/views/PreviewCursor.tsx26
-rw-r--r--src/client/views/PropertiesView.scss89
-rw-r--r--src/client/views/PropertiesView.tsx205
-rw-r--r--src/client/views/ScriptingRepl.tsx3
-rw-r--r--src/client/views/StyleProvider.tsx2
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx20
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx2
-rw-r--r--src/client/views/collections/TabDocView.tsx2
-rw-r--r--src/client/views/collections/TreeView.tsx18
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx4
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.tsx30
-rw-r--r--src/client/views/nodes/EquationBox.tsx7
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBadge.scss5
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx59
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts18
-rw-r--r--src/client/views/search/SearchBox.tsx2
-rw-r--r--src/client/views/topbar/TopBar.tsx16
-rw-r--r--src/fields/CursorField.ts41
-rw-r--r--src/fields/Doc.ts61
-rw-r--r--src/fields/DocSymbols.ts3
-rw-r--r--src/fields/FieldSymbols.ts2
-rw-r--r--src/fields/List.ts459
-rw-r--r--src/fields/ObjectField.ts15
-rw-r--r--src/fields/SchemaHeaderField.ts14
-rw-r--r--src/fields/util.ts321
49 files changed, 1531 insertions, 1123 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index be99aa5af..06389d6ae 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 2a7f5a09b..fa1fca6ff 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -76,10 +76,14 @@ export namespace DocServer {
const fieldWriteModes: { [field: string]: WriteMode } = {};
const docsWithUpdates: { [field: string]: Set<Doc> } = {};
- export var PlaygroundFields: string[];
- export function setPlaygroundFields(livePlaygroundFields: string[]) {
- DocServer.PlaygroundFields = livePlaygroundFields;
- livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground));
+ export var PlaygroundFields: string[] = [];
+ export function setLivePlaygroundFields(livePlaygroundFields: string[]) {
+ DocServer.PlaygroundFields.push(...livePlaygroundFields);
+ livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
+ }
+ export function setPlaygroundFields(playgroundFields: string[]) {
+ DocServer.PlaygroundFields.push(...playgroundFields);
+ playgroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground));
}
export function IsPlaygroundField(field: string) {
return DocServer.PlaygroundFields?.includes(field.replace(/^_/, ''));
@@ -97,7 +101,7 @@ export namespace DocServer {
}
export function getFieldWriteMode(field: string) {
- return Doc.CurrentUserEmail === 'guest' ? WriteMode.LiveReadonly : fieldWriteModes[field] || WriteMode.Default;
+ return Doc.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default;
}
export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) {
@@ -203,7 +207,7 @@ export namespace DocServer {
}
export function makeEditable() {
- if (_isReadOnly) {
+ if (Control.isReadOnly()) {
location.reload();
// _isReadOnly = false;
// _CreateField = _CreateFieldImpl;
@@ -479,7 +483,7 @@ export namespace DocServer {
function _CreateFieldImpl(field: RefField) {
_cache[field[Id]] = field;
const initialState = SerializationHelper.Serialize(field);
- Utils.Emit(_socket, MessageStore.CreateField, initialState);
+ Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.CreateField, initialState);
}
let _CreateField: (field: RefField) => void = errorFunc;
@@ -499,7 +503,7 @@ export namespace DocServer {
}
function _UpdateFieldImpl(id: string, diff: any) {
- !DocServer.Control.isReadOnly() && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ !DocServer.Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
}
let _UpdateField: (id: string, diff: any) => void = errorFunc;
@@ -536,11 +540,11 @@ export namespace DocServer {
}
export function DeleteDocument(id: string) {
- Utils.Emit(_socket, MessageStore.DeleteField, id);
+ Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.DeleteField, id);
}
export function DeleteDocuments(ids: string[]) {
- Utils.Emit(_socket, MessageStore.DeleteFields, ids);
+ Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.DeleteFields, ids);
}
function _respondToDeleteImpl(ids: string | string[]) {
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index cdc8c275c..6cd8de87a 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -42,7 +42,6 @@ export enum DocumentType {
COMPARISON = 'comparison',
GROUP = 'group',
- LINKDB = 'linkdb', // database of links ??? why do we have this
SCRIPTDB = 'scriptdb', // database of scripts
GROUPDB = 'groupdb', // database of groups
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c925cff0f..665165c70 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -169,8 +169,8 @@ export class DocumentOptions {
_nativeDimModifiable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers');
_nativeHeightUnfrozen?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers');
- 'acl-Public'?: string; // public permissions
- '_acl-Public'?: string; // public permissions
+ 'acl-Guest'?: string; // public permissions
+ '_acl-Guest'?: string; // public permissions
type?: DTYPEt = new DTypeInfo('type of document', true);
type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection
_type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection
@@ -396,6 +396,7 @@ export class DocumentOptions {
sidebar_color?: string; // background color of text sidebar
sidebar_collectionType?: string; // collection type of text sidebar
+ data_dashboards?: List<any>; // list of dashboards used in shareddocs;
text?: string;
textTransform?: string;
letterSpacing?: string;
@@ -545,14 +546,6 @@ export namespace Docs {
},
],
[
- DocumentType.LINKDB,
- {
- data: new List<Doc>(),
- layout: { view: EmptyBox, dataField: defaultDataKey },
- options: { title: 'Global Link Database' },
- },
- ],
- [
DocumentType.SCRIPTDB,
{
data: new List<Doc>(),
@@ -669,7 +662,6 @@ export namespace Docs {
[
DocumentType.GROUPDB,
{
- data: new List<Doc>(),
layout: { view: EmptyBox, dataField: defaultDataKey },
options: { title: 'Global Group Database' },
},
@@ -724,7 +716,7 @@ export namespace Docs {
.filter(type => type !== DocumentType.NONE)
.map(type => type + suffix);
// fetch the actual prototype documents from the server
- const actualProtos = Docs.newAccount ? {} : await DocServer.GetRefFields(prototypeIds);
+ const actualProtos = await DocServer.GetRefFields(prototypeIds);
// update this object to include any default values: DocumentOptions for all prototypes
prototypeIds.map(id => {
const existing = actualProtos[id] as Doc;
@@ -749,13 +741,6 @@ export namespace Docs {
}
/**
- * A collection of all links in the database. Ideally, this would be a search, but for now all links are cached here.
- */
- export function MainLinkDocument() {
- return Prototypes.get(DocumentType.LINKDB);
- }
-
- /**
* A collection of all scripts in the database
*/
export function MainScriptDocument() {
@@ -802,6 +787,7 @@ export namespace Docs {
x: 0,
y: 0,
_width: 300,
+ 'acl-Guest': SharingPermissions.View,
...(template.options || {}),
layout: layout.view?.LayoutString(layout.dataField),
data: template.data,
@@ -843,8 +829,7 @@ export namespace Docs {
const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_');
// dataProps['acl-Override'] = SharingPermissions.Unset;
- dataProps['acl-Public'] = options['acl-Public'] ? options['acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
-
+ dataProps['acl-Guest'] = options['acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View);
dataProps.isSystem = viewProps.isSystem;
dataProps.isDataDoc = true;
dataProps.author = Doc.CurrentUserEmail;
@@ -866,16 +851,17 @@ export namespace Docs {
dataDoc.proto = proto;
}
- const viewFirstProps: { [id: string]: any } = {};
- viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
- // viewFirstProps['acl-Override'] = SharingPermissions.Unset;
- viewFirstProps.author = Doc.CurrentUserEmail;
+ const viewFirstProps: { [id: string]: any } = { author: Doc.CurrentUserEmail };
+ viewFirstProps['acl-Guest'] = options['_acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View);
let viewDoc: Doc;
// determines whether viewDoc should be created using placeholder Doc or default
if (placeholderDoc) {
placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined;
placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined;
viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
+ Array.from(Object.keys(placeholderDoc))
+ .filter(key => key.startsWith('acl'))
+ .forEach(key => (dataDoc[key] = viewDoc[key] = placeholderDoc[key]));
} else {
viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
}
@@ -1015,10 +1001,11 @@ export namespace Docs {
I.rotation = 0;
I.defaultDoubleClick = 'click';
I.author_date = new DateField();
- I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
+ I['acl-Guest'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
//I['acl-Override'] = SharingPermissions.Unset;
I[Initializing] = false;
- return I;
+
+ return InstanceFromProto(I, '', options);
}
export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
@@ -1054,7 +1041,7 @@ export namespace Docs {
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _type_collection: CollectionViewType.Freeform }, id);
- documents.forEach(d => (d.embedContainer = inst));
+ documents.forEach(d => Doc.SetContainer(d, inst));
return inst;
}
@@ -1127,8 +1114,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.LABEL), undefined, { ...(options || {}) });
}
- export function EquationDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), undefined, { ...(options || {}) }, undefined, 'text');
+ export function EquationDocument(text?: string, options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), text, { ...(options || {}) }, undefined, 'text');
}
export function FunctionPlotDocument(documents: Array<Doc>, options?: DocumentOptions) {
@@ -1159,7 +1146,9 @@ export namespace Docs {
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeViewFreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
+ const ret = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeViewFreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
+ documents.map(c => Doc.SetContainer(c, ret));
+ return ret;
}
export function DirectoryImportDocument(options: DocumentOptions = {}) {
@@ -1184,10 +1173,13 @@ export namespace Docs {
const doc = DockDocument(
configs.map(c => c.doc),
JSON.stringify(layoutConfig),
- options,
+ Doc.CurrentUserEmail === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options },
id
);
- configs.map(c => (c.doc.embedContainer = doc));
+ configs.map(c => {
+ Doc.SetContainer(c.doc, doc);
+ inheritParentAcls(doc, c.doc, false);
+ });
return doc;
}
@@ -1347,8 +1339,8 @@ export namespace DocUtils {
source,
target,
{
- 'acl-Public': SharingPermissions.Augment,
- '_acl-Public': SharingPermissions.Augment,
+ 'acl-Guest': SharingPermissions.Augment,
+ '_acl-Guest': SharingPermissions.Augment,
title: ComputedField.MakeFunction('generateLinkTitle(self)') as any,
link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined,
link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined,
@@ -1869,8 +1861,6 @@ export namespace DocUtils {
Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString(), true);
}
- if (ndoc && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, ndoc);
-
return ndoc;
}
export function delegateDragFactory(dragFactory: Doc) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index cde3daf5a..4481933d5 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -265,7 +265,7 @@ export class CurrentUserUtils {
}[] = [
{key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }},
{key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_enableAltContentUI: true}},
- {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, }},
+ {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 300, _height: 35, }},
{key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}},
{key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }},
{key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }},
@@ -767,7 +767,7 @@ export class CurrentUserUtils {
linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail;
linkDocs.author = Doc.CurrentUserEmail;
linkDocs.data = new List<Doc>([]);
- linkDocs["acl-Public"] = SharingPermissions.Augment;
+ linkDocs["acl-Guest"] = SharingPermissions.Augment;
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
}
}
@@ -786,11 +786,11 @@ export class CurrentUserUtils {
title: "My Shared Docs",
userColor: "rgb(202, 202, 202)",
isFolder:true, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
- childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
- childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
- childContextMenuLabels: new List<string>(["Add to Dashboards",]),
- childContextMenuIcons: new List<string>(["user-plus",]),
- "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
+ // childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
+ // childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
+ // childContextMenuLabels: new List<string>(["Add to Dashboards",]),
+ // childContextMenuIcons: new List<string>(["user-plus",]),
+ "acl-Guest": SharingPermissions.Augment, "_acl-Guest": SharingPermissions.Augment,
childDragAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
// NOTE: treeViewHideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar
_layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
@@ -798,6 +798,7 @@ export class CurrentUserUtils {
};
DocUtils.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts);
+ if (!DocCast(doc.mySharedDocs).data_dashboards) DocCast(doc.mySharedDocs).data_dashboards = new List<Doc>();
}
/// Import option on the left side button panel
@@ -825,7 +826,7 @@ export class CurrentUserUtils {
async () => {
const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data);
const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
- SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]);
+ SetCachedGroups(["Guest", ...mygroups?.map(g => StrCast(g.title))]);
}, { fireImmediately: true });
doc.isSystem ?? (doc.isSystem = true);
doc.title ?? (doc.title = Doc.CurrentUserEmail);
@@ -856,6 +857,7 @@ export class CurrentUserUtils {
doc.userTheme ?? (doc.userTheme = ColorScheme.Dark);
doc.filterDocCount = 0;
doc.treeViewFreezeChildren = "remove|add";
+ doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage;
this.setupLinkDocs(doc, linkDatabaseId);
this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 3f0848d00..b921b3116 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -194,7 +194,7 @@ export class DocumentManager {
var containerDocContext = srcContext ? [srcContext, doc] : [doc];
while (
containerDocContext.length &&
- containerDocContext[0]?.embedContainer &&
+ DocCast(containerDocContext[0]?.embedContainer) &&
DocCast(containerDocContext[0].embedContainer)?._type_collection !== CollectionViewType.Docking &&
(includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0]))
) {
diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss
index 70fa88f6d..253ed5d2a 100644
--- a/src/client/util/GroupManager.scss
+++ b/src/client/util/GroupManager.scss
@@ -1,6 +1,7 @@
.group-interface {
width: 380px;
height: 300px;
+ position: relative;
.dialogue-box {
.group-create {
@@ -56,8 +57,9 @@
flex-direction: column;
.overlay {
- transform: translate(-20px, -20px);
- border-radius: 10px;
+ transform: translate(-10px, -10px);
+ width: 400px;
+ height: 320px;
}
.delete-button {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index f74409e42..3e526c4c0 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -103,7 +103,7 @@ export class LinkFollower {
Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target);
movedTarget = true;
}
- target.embedContainer = sourceDocParent;
+ Doc.SetContainer(target, sourceDocParent);
const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
if (sourceDoc.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
target.x = moveTo[0];
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index ce422f849..c7f092565 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -46,38 +46,41 @@ export class LinkManager {
LinkManager._instance = this;
this.createlink_relationshipLists();
LinkManager.userLinkDBs = [];
- const addLinkToDoc = (link: Doc) => {
- Promise.all([link]).then(linkdoc => {
- const link = DocCast(linkdoc[0]);
- Promise.all([link.proto]).then(linkproto => {
- Promise.all([link.link_anchor_1, link.link_anchor_2]).then(linkdata => {
- const a1 = DocCast(linkdata[0]);
- const a2 = DocCast(linkdata[1]);
- a1 &&
- a2 &&
- Promise.all([Doc.GetProto(a1), Doc.GetProto(a2)]).then(
- action(protos => {
- (protos[0] as Doc)?.[DirectLinks].add(link);
- (protos[1] as Doc)?.[DirectLinks].add(link);
+ // since this is an action, not a reaction, we get only one shot to add this link to the Anchor docs
+ // Thus make sure all promised values are resolved from link -> link.proto -> link.link_anchor_[1,2] -> link.link_anchor_[1,2].proto
+ // Then add the link to the anchor protos.
+ const addLinkToDoc = (lprom: Doc) =>
+ PromiseValue(lprom).then((link: Opt<Doc>) =>
+ PromiseValue(link?.proto as Doc).then((lproto: Opt<Doc>) =>
+ Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) =>
+ Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) =>
+ Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then(
+ action(lAnchProtoProtos => {
+ link && lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].add(link);
+ link && lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].add(link);
})
- );
- });
- });
- });
- };
- const remLinkFromDoc = (link: Doc) => {
- const a1 = link?.link_anchor_1;
- const a2 = link?.link_anchor_2;
- Promise.all([a1, a2]).then(
- action(() => {
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinks].delete(link);
- Doc.GetProto(a2)[DirectLinks].delete(link);
- Doc.GetProto(link)[DirectLinks].delete(link);
- }
- })
+ )
+ )
+ )
+ )
);
- };
+
+ const remLinkFromDoc = (lprom: Doc) =>
+ PromiseValue(lprom).then((link: Opt<Doc>) =>
+ PromiseValue(link?.proto as Doc).then((lproto: Opt<Doc>) =>
+ Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) =>
+ Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) =>
+ Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then(
+ action(lAnchProtoProtos => {
+ link && lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].delete(link);
+ link && lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].delete(link);
+ })
+ )
+ )
+ )
+ )
+ );
+
const watchUserLinkDB = (userLinkDBDoc: Doc) => {
LinkManager._links.push(...DocListCast(userLinkDBDoc.data));
const toRealField = (field: Field) => (field instanceof ProxyField ? field.value : field); // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields
@@ -156,7 +159,7 @@ export class LinkManager {
return this.relatedLinker(anchor);
} // finds all links that contain the given anchor
public getAllDirectLinks(anchor: Doc): Doc[] {
- return Array.from(Doc.GetProto(anchor)[DirectLinks] ?? []);
+ return Array.from(Doc.GetProto(anchor)[DirectLinks]);
} // finds all links that contain the given anchor
relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
index 247267710..b93d4f293 100644
--- a/src/client/util/RTFMarkup.tsx
+++ b/src/client/util/RTFMarkup.tsx
@@ -52,7 +52,7 @@ export class RTFMarkup extends React.Component<{}> {
{` add a sidebar text document inline`}
</p>
<p>
- <b style={{ fontSize: 'larger' }}>{`\`\` `}</b>
+ <b style={{ fontSize: 'larger' }}>{`\`\`\` `}</b>
{` create a code snippet block`}
</p>
<p>
diff --git a/src/client/util/ScriptManager.ts b/src/client/util/ScriptManager.ts
index 42a6493ea..87509f2ea 100644
--- a/src/client/util/ScriptManager.ts
+++ b/src/client/util/ScriptManager.ts
@@ -1,12 +1,11 @@
-import { Doc, DocListCast } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
-import { Cast, StrCast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
-import { ScriptingGlobals } from "./ScriptingGlobals";
+import { Doc, DocListCast } from '../../fields/Doc';
+import { List } from '../../fields/List';
+import { listSpec } from '../../fields/Schema';
+import { Cast, StrCast } from '../../fields/Types';
+import { Docs } from '../documents/Documents';
+import { ScriptingGlobals } from './ScriptingGlobals';
export class ScriptManager {
-
static _initialized = false;
private static _instance: ScriptManager;
public static get Instance(): ScriptManager {
@@ -24,11 +23,7 @@ export class ScriptManager {
}
public getAllScripts(): Doc[] {
const sdoc = ScriptManager.Instance.ScriptManagerDoc;
- if (sdoc) {
- const docs = DocListCast(sdoc.data);
- return docs;
- }
- return [];
+ return sdoc ? DocListCast(sdoc.data) : [];
}
public addScript(scriptDoc: Doc): boolean {
@@ -59,36 +54,35 @@ export class ScriptManager {
}
public static addScriptToGlobals(scriptDoc: Doc): void {
-
ScriptingGlobals.removeGlobal(StrCast(scriptDoc.name));
- const params = Cast(scriptDoc["data-params"], listSpec("string"), []);
+ const params = Cast(scriptDoc['data-params'], listSpec('string'), []);
const paramNames = params.reduce((o: string, p: string) => {
if (params.indexOf(p) === params.length - 1) {
- o = o + p.split(":")[0].trim();
+ o = o + p.split(':')[0].trim();
} else {
- o = o + p.split(":")[0].trim() + ",";
+ o = o + p.split(':')[0].trim() + ',';
}
return o;
- }, "" as string);
+ }, '' as string);
const f = new Function(paramNames, StrCast(scriptDoc.script));
Object.defineProperty(f, 'name', { value: StrCast(scriptDoc.name), writable: false });
- let parameters = "(";
+ let parameters = '(';
params.forEach((element: string, i: number) => {
if (i === params.length - 1) {
- parameters = parameters + element + ")";
+ parameters = parameters + element + ')';
} else {
- parameters = parameters + element + ", ";
+ parameters = parameters + element + ', ';
}
});
- if (parameters === "(") {
+ if (parameters === '(') {
ScriptingGlobals.add(f, StrCast(scriptDoc.description));
} else {
ScriptingGlobals.add(f, StrCast(scriptDoc.description), parameters);
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 1be27fa62..05401e2cd 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -6,7 +6,7 @@
transform: translate(-20px, -20px);
}
- select {
+ .select {
text-align: justify;
text-align-last: end
}
@@ -21,7 +21,30 @@
top: 2px;
}
+ .share-title {
+ display: inline-flex;
+ gap: 5px;
+
+ .share-info {
+ align-self: center;
+ cursor: pointer;
+ }
+ }
+
+ .share-copy-link {
+ display: inline;
+ border-radius: 4px;
+ border: solid gray 1px;
+ font-size: x-small;
+ background: #E8E8E8;
+ color: black;
+ margin-top: -15px;
+ margin-bottom: 15px;
+ width: fit-content;
+ }
+
.share-container {
+
.share-setup {
display: flex;
margin-bottom: 20px;
@@ -42,11 +65,15 @@
outline: none;
text-align: justify; // for Edge
text-align-last: end;
+ font-size: 13px;
+ min-width: 90px;
+ height: 36;
+ margin-left: 2px;
}
.share-button {
- height: 105%;
- margin-left: 2%;
+ height: 36;
+ margin-left: 3%;
background-color: black;
}
}
@@ -74,15 +101,16 @@
float: right;
align-items: baseline;
margin-top: -12;
+ margin-bottom: 10;
.layoutDoc-acls,
.myDocs-acls {
flex-direction: column;
- margin-right: 12;
label {
font-weight: normal;
font-style: italic;
+ padding-right: 12;
}
input {
@@ -100,6 +128,7 @@
.group-container {
width: 50%;
display: flex;
+ top:0;
flex-direction: column;
.user-sort {
@@ -118,9 +147,10 @@
.users-list {
font-style: italic;
background: #e8e8e8;
+ border: 2px solid gray;
padding-left: 10px;
padding-right: 10px;
- width: 100%;
+ width: 97%;
overflow-y: scroll;
overflow-x: hidden;
text-align: left;
@@ -188,54 +218,150 @@
}
}
+ .title-individual{
+ height: 25px;
+ padding-left: 2;
+ width: 97%;
+ margin-top: 10px;
+ margin-left: -8px;
+ font-size: 14;
+ margin-bottom: -4;
+ border: 2px solid gray;
+ border-bottom: none;
+ align-items: center;
+ display: flex;
+ }
+
+ .title-group{
+ height: 25px;
+ padding-left: 2;
+ width: 97%;
+ margin-top: 10px;
+ margin-left: -.5px;
+ font-size: 14;
+ margin-bottom: -4;
+ border: 2px solid gray;
+ border-bottom: none;
+ align-items: center;
+ display: flex;
+ }
+
.container {
display: flex;
position: relative;
margin-top: 5px;
- margin-bottom: 10px;
+ margin-left: -5px;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- width: 100%;
+ width: 97%;
text-align: left;
font-style: normal;
- font-size: 14;
+ font-size: 12.5;
font-weight: normal;
- padding: 0;
- align-items: center;
+
+ padding: 3px;
+ border-bottom: 0.5px solid grey;
.group-info {
cursor: pointer;
}
&:hover .padding {
+ overflow-x: unset;
white-space: unset;
+ overflow-wrap: break-word;
}
.padding {
- padding: 0 10px 0 0;
- color: black;
+ max-width: 150px;
+ overflow-x: hidden;
+ display: inline-block;
text-overflow: ellipsis;
- overflow: hidden;
white-space: nowrap;
- max-width: 40%;
}
.permissions-dropdown {
- border: none;
- height: 25;
- background-color: #e8e8e8;
+ display: flex;
+ align-items: flex-end;
+ text-align: right;
+ margin-left: auto;
+ margin-right: -12px;
+
}
.edit-actions {
display: flex;
position: absolute;
- right: -10;
+ align-items: flex-end;
+ right: -10;
}
+ }
+
+ .permissions-dropdown-None{
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ padding: 0px;
+ padding-left: 3px;
+ background: grey;
+ color: rgb(71, 71, 71);
+ border-radius: 6px;
+ border: 1px solid rgb(71, 71, 71);
+ }
+ .permissions-dropdown-Edit,
+ .permissions-dropdown-Admin {
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ 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);
+ }
+ .permissions-dropdown-Augment{
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ 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);
}
+ .permissions-dropdown-View{
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ 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);
+ }
+ .permissions-dropdown-Not-Shared{
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ 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);
+ }
.no-users {
margin-top: 20px;
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 97e64ab71..8e5ae7bc0 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,15 +1,15 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { intersection } from 'lodash';
+import { Colors } from 'browndash-components';
+import { concat, intersection } from 'lodash';
import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import Select from 'react-select';
import * as RequestPromise from 'request-promise';
-import { Doc, DocListCast, DocListCastAsync, HierarchyMapping } from '../../fields/Doc';
-import { AclAdmin, AclPrivate, DocAcl, AclUnset, DocData } from '../../fields/DocSymbols';
+import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
+import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
-import { List } from '../../fields/List';
-import { NumCast, StrCast } from '../../fields/Types';
+import { StrCast } from '../../fields/Types';
import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
import { Utils } from '../../Utils';
import { DocServer } from '../DocServer';
@@ -21,8 +21,10 @@ import { SearchBox } from '../views/search/SearchBox';
import { DocumentManager } from './DocumentManager';
import { GroupManager, UserOptions } from './GroupManager';
import { GroupMemberView } from './GroupMemberView';
+import { LinkManager } from './LinkManager';
import { SelectionManager } from './SelectionManager';
import './SharingManager.scss';
+import { undoable } from './UndoManager';
export interface User {
email: string;
@@ -47,6 +49,7 @@ const indType = '!indType/';
const groupType = '!groupType/';
const storage = 'data';
+const dashStorage = 'data_dashboards';
/**
* A user who also has a sharing doc.
@@ -73,13 +76,14 @@ export class SharingManager extends React.Component<{}> {
@observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals
@observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups
private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup
- private distributeAclsButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the distribute button, used for the position of the popup
// if both showUserOptions and showGroupOptions are false then both are displayed
@observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component)
@observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component)
private populating: boolean = false; // whether the list of users is populating or not
+ @observable private overrideNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is override
@observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
@observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not
+ @observable private _buttonDown = false;
// private get linkVisible() {
// return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
@@ -93,6 +97,7 @@ export class SharingManager extends React.Component<{}> {
DictationOverlay.Instance.hasActiveModal = true;
this.isOpen = this.targetDoc !== undefined;
this.permissions = SharingPermissions.Augment;
+ this.overrideNested = true;
});
};
@@ -108,6 +113,7 @@ export class SharingManager extends React.Component<{}> {
}),
500
);
+ this.layoutDocAcls = false;
});
constructor(props: {}) {
@@ -138,7 +144,7 @@ export class SharingManager extends React.Component<{}> {
if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
if (!this.users.find(user => user.user.email === newUser.email)) {
this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
- // LinkManager.addLinkDB(sharer.linkDatabase);
+ LinkManager.addLinkDB(linkDatabase);
}
}
})
@@ -150,79 +156,44 @@ export class SharingManager extends React.Component<{}> {
/**
* Shares the document with a user.
*/
- setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => {
+ setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => {
const { user, sharingDoc } = recipient;
const target = targetDoc || this.targetDoc!;
const acl = `acl-${normalizeEmail(user.email)}`;
- const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`;
- const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1;
-
- const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
- return !docs
- .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
- .map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
-
- if (permission === SharingPermissions.None) {
- if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1;
- } else {
- if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1;
- }
-
- distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
-
- this.setDashboardBackground(doc, permission as SharingPermissions);
- if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc);
- return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc);
- })
- .some(success => !success);
- };
+ const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc);
+ docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => {
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.overrideNested ? true : undefined);
+ if (permission !== SharingPermissions.None) {
+ Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc);
+ } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc);
+ });
+ }, 'set Doc permissions');
/**
* Sets the permission on the target for the group.
* @param group
* @param permission
*/
- setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
+ setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
const target = targetDoc || this.targetDoc!;
- const key = normalizeEmail(StrCast(group.title));
- const acl = `acl-${key}`;
- const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1;
-
- const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
-
- // ! ensures it returns true if document has been shared successfully, false otherwise
- return !docs
- .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
- .map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
-
- if (permission === SharingPermissions.None) {
- if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1;
- } else {
- if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1;
- }
+ const acl = `acl-${normalizeEmail(StrCast(group.title))}`;
- distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
- this.setDashboardBackground(doc, permission as SharingPermissions);
+ const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc);
+ docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => {
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.overrideNested ? true : undefined);
- if (group instanceof Doc) {
- const members: string[] = JSON.parse(StrCast(group.members));
- const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
+ if (group instanceof Doc) {
+ Doc.AddDocToList(group, 'docsShared', doc);
- // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc
- group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : (group.docsShared = new List<Doc>([doc]));
-
- return users
- .map(({ user, sharingDoc }) => {
- if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
- else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists
- })
- .some(success => !success);
- }
- })
- .some(success => success);
- };
+ this.users
+ .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email))
+ .forEach(({ user, sharingDoc }) => {
+ if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added
+ else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists
+ });
+ }
+ });
+ }, 'set group permissions');
/**
* Shares the documents shared with a group with a new user who has been added to that group.
@@ -237,7 +208,13 @@ export class SharingManager extends React.Component<{}> {
else {
DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
DocListCastAsync(group.docsShared).then(dl => {
- const filtered = dl?.filter(doc => !userdocs?.includes(doc));
+ const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc));
+ filtered && userdocs?.push(...filtered);
+ })
+ );
+ DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs =>
+ DocListCastAsync(group.docsShared).then(dl => {
+ const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc));
filtered && userdocs?.push(...filtered);
})
);
@@ -248,44 +225,23 @@ export class SharingManager extends React.Component<{}> {
/**
* Called from the properties sidebar to change permissions of a user.
*/
- shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[]) => {
- if (shareWith !== 'Public' && shareWith !== 'Override') {
+ shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => {
+ if (layout) this.layoutDocAcls = true;
+ if (shareWith !== 'Guest') {
const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? Doc.CurrentUserEmail : shareWith));
docs.forEach(doc => {
if (user) this.setInternalSharing(user, permission, doc);
- else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc);
+ else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true);
});
} else {
- const dashboards = DocListCast(Doc.MyDashboards.data);
docs.forEach(doc => {
- const isDashboard = dashboards.indexOf(doc) !== -1;
- if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard);
- this.setDashboardBackground(doc, permission as SharingPermissions);
- });
- }
- };
-
- /**
- * Sets the background of the Dashboard if it has been shared as a visual indicator
- */
- setDashboardBackground = (doc: Doc, permission: SharingPermissions) => {
- if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) {
- if (permission !== SharingPermissions.None) {
- doc.isShared = true;
- doc.backgroundColor = 'green';
- } else {
- const acls = doc[DocData][DocAcl];
- if (
- Object.keys(acls)
- .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && key !== 'acl-Me')
- .every(key => [AclUnset, AclPrivate].includes(acls[key]))
- ) {
- doc.isShared = undefined;
- doc.backgroundColor = undefined;
+ if (GetEffectiveAcl(doc) === AclAdmin) {
+ distributeAcls(`acl-${shareWith}`, permission, doc, undefined);
}
- }
+ });
}
- };
+ this.layoutDocAcls = false;
+ }, 'sidebar set permissions');
/**
* Removes the documents shared with a user through a group when the user is removed from the group.
@@ -295,13 +251,19 @@ export class SharingManager extends React.Component<{}> {
removeMember = (group: Doc, emailId: string) => {
const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
- if (group.docsShared) {
+ if (group.docsShared && user) {
DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
DocListCastAsync(group.docsShared).then(dl => {
const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || [];
userdocs?.splice(0, userdocs.length, ...remaining);
})
);
+ DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs =>
+ DocListCastAsync(group.docsShared).then(dl => {
+ const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || [];
+ userdocs?.splice(0, userdocs.length, ...remaining);
+ })
+ );
}
};
@@ -311,11 +273,9 @@ export class SharingManager extends React.Component<{}> {
*/
removeGroup = (group: Doc) => {
if (group.docsShared) {
- const dashboards = DocListCast(Doc.MyDashboards.data);
DocListCast(group.docsShared).forEach(doc => {
const acl = `acl-${StrCast(group.title)}`;
- const isDashboard = dashboards.indexOf(doc) !== -1;
- distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined, isDashboard);
+ distributeAcls(acl, SharingPermissions.None, doc);
const members: string[] = JSON.parse(StrCast(group.members));
const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
@@ -331,37 +291,26 @@ export class SharingManager extends React.Component<{}> {
// return;
// }
// targetDoc["acl-" + PublicKey] = permission;
- // }
+ // }s
- // private get sharingUrl() {
- // if (!this.targetDoc) {
- // return undefined;
- // }
- // const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
- // return `${baseUrl}?sharing=true`;
- // }
-
- // copy = action(() => {
- // if (this.sharingUrl) {
- // Utils.CopyText(this.sharingUrl);
- // this.copied = true;
- // }
- // });
+ /**
+ * Copies the Public sharing url to the user's clipboard.
+ */
+ private copyURL = (e: any) => {
+ Utils.CopyText(Utils.shareUrl(this.targetDoc![Id]));
+ };
/**
* Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share
*/
- private sharingOptions(uniform: boolean, override?: boolean) {
- const dropdownValues: string[] = Object.values(SharingPermissions);
+ private sharingOptions(uniform: boolean, showGuestOptions?: boolean) {
+ const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions);
if (!uniform) dropdownValues.unshift('-multiple-');
- if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1);
- return dropdownValues
- .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any))
- .map(permission => (
- <option key={permission} value={permission}>
- {permission}
- </option>
- ));
+ return dropdownValues.map(permission => (
+ <option key={permission} value={permission}>
+ {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)}
+ </option>
+ ));
}
private focusOn = (contents: string) => {
@@ -406,38 +355,43 @@ export class SharingManager extends React.Component<{}> {
/**
* Handles changes in the permission chosen to share with someone with
*/
- @action
- handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
- this.permissions = event.currentTarget.value as SharingPermissions;
- };
+ handlePermissionsChange = undoable(
+ action((event: React.ChangeEvent<HTMLSelectElement>) => {
+ this.permissions = event.currentTarget.value as SharingPermissions;
+ }),
+ 'permission change'
+ );
/**
* Calls the relevant method for sharing, displays the popup, and resets the relevant variables.
*/
- @action
- share = () => {
- if (this.selectedUsers) {
- this.selectedUsers.forEach(user => {
- if (user.value.includes(indType)) {
- this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions);
- } else {
- this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
- }
- });
-
- const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
- TaskCompletionBox.popupX = left - 1.5 * width;
- TaskCompletionBox.popupY = top - 1.5 * height;
- TaskCompletionBox.textDisplayed = 'Document shared!';
- TaskCompletionBox.taskCompleted = true;
- setTimeout(
- action(() => (TaskCompletionBox.taskCompleted = false)),
- 2000
- );
+ share = undoable(
+ action(() => {
+ if (this.selectedUsers) {
+ this.selectedUsers.forEach(user => {
+ if (user.value.includes(indType)) {
+ this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined);
+ } else {
+ this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
+ }
+ });
+
+ const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
+ TaskCompletionBox.popupX = left - 1.5 * width;
+ TaskCompletionBox.popupY = top - 1.5 * height;
+ TaskCompletionBox.textDisplayed = 'Document shared!';
+ TaskCompletionBox.taskCompleted = true;
+ setTimeout(
+ action(() => (TaskCompletionBox.taskCompleted = false)),
+ 2000
+ );
- this.selectedUsers = null;
- }
- };
+ this.layoutDocAcls = false;
+ this.selectedUsers = null;
+ }
+ }),
+ 'share Doc'
+ );
/**
* Sorting algorithm to sort users.
@@ -464,6 +418,7 @@ export class SharingManager extends React.Component<{}> {
if (!this.targetDoc) return null;
TraceMobx();
const groupList = GroupManager.Instance?.allGroups || [];
+
const sortedUsers = this.users
.slice()
.sort(this.sortUsers)
@@ -485,8 +440,7 @@ export class SharingManager extends React.Component<{}> {
const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users;
const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
- // handles the case where multiple documents are selected
- let docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DocData]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DocData]));
+ let docs = SelectionManager.Views().length < 2 ? [this.targetDoc] : SelectionManager.Views().map(docView => docView.rootDoc);
if (this.myDocAcls) {
const newDocs: Doc[] = [];
@@ -501,26 +455,32 @@ export class SharingManager extends React.Component<{}> {
const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin);
// users in common between all docs
- const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl])));
+ const commonKeys = intersection(docs).reduce((list, doc) => (doc?.[DocAcl] ? [...list, ...Object.keys(doc[DocAcl])] : list), [] as string[]);
// the list of users shared with
- const userListContents: (JSX.Element | null)[] = users
- .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
+ const userListContents = users
+ // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
+ .filter(({ user }) => docs[0]?.author !== user.email)
.map(({ user, linkDatabase, sharingDoc, userColor }) => {
const userKey = `acl-${normalizeEmail(user.email)}`;
- const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
- const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
+ const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
+ // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
+ let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]);
+ permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
return !permissions ? null : (
<div key={userKey} className={'container'}>
<span className={'padding'}>{user.email}</span>
<div className="edit-actions">
{admin || this.myDocAcls ? (
- <select className={'permissions-dropdown'} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}>
+ <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}>
{this.sharingOptions(uniform)}
</select>
) : (
- <div className={'permissions-dropdown'}>{permissions}</div>
+ <div className={`permissions-dropdown-${permissions}`}>
+ {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)}
+ &nbsp;
+ </div>
)}
</div>
</div>
@@ -531,6 +491,9 @@ export class SharingManager extends React.Component<{}> {
const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author);
// the owner of the doc and the current user are placed at the top of the user list.
+ const userKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`;
+ const curUserPermission = StrCast(targetDoc[userKey]);
+ // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name
userListContents.unshift(
sameAuthor ? (
<div key={'owner'} className={'container'}>
@@ -544,7 +507,10 @@ export class SharingManager extends React.Component<{}> {
<div key={'me'} className={'container'}>
<span className={'padding'}>Me</span>
<div className="edit-actions">
- <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-'}</div>
+ <div className={`permissions-dropdown-${curUserPermission}`}>
+ {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'}
+ &nbsp;
+ </div>
</div>
</div>
) : null
@@ -552,29 +518,31 @@ export class SharingManager extends React.Component<{}> {
// the list of groups shared with
const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true));
- groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" });
+ groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" });
const groupListContents = groupListMap.map(group => {
- const groupKey = `acl-${StrCast(group.title)}`;
- const uniform = docs
- .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
- .every(doc => (this.layoutDocAcls ? doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey] : doc?.[DocData]?.[DocAcl]?.[groupKey] === docs[0]?.[DocData]?.[DocAcl]?.[groupKey]));
- const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-';
+ let groupKey = `acl-${StrCast(group.title)}`;
+ const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]);
+ const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-';
return !permissions ? null : (
<div key={groupKey} className={'container'}>
<div className={'padding'}>{StrCast(group.title)}</div>
+ &nbsp;
{group instanceof Doc ? (
<div className="group-info" onClick={action(() => (GroupManager.Instance.currentGroup = group))}>
<FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} />
</div>
) : null}
- <div className="edit-actions">
+ <div className={'edit-actions'}>
{admin || this.myDocAcls ? (
- <select className={'permissions-dropdown'} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}>
- {this.sharingOptions(uniform, group.title === 'Override')}
+ <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}>
+ {this.sharingOptions(uniform, group.title === 'Guest')}
</select>
) : (
- <div className={'permissions-dropdown'}>{permissions}</div>
+ <div className={`permissions-dropdown-${permissions}`}>
+ {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)}
+ &nbsp;
+ </div>
)}
</div>
</div>
@@ -584,19 +552,26 @@ export class SharingManager extends React.Component<{}> {
<div className="sharing-interface">
{GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null}
<div className="sharing-contents">
- <p className={'share-title'}>
+ <p className="share-title">
+ <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties/sharing-and-permissions/', '_blank')}>
+ <FontAwesomeIcon icon={'question-circle'} size={'sm'} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties/sharing-and-permissions/', '_blank')} />
+ </div>
<b>Share </b>
{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}
</p>
- <div className={'close-button'} onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} />
+ <button
+ className="share-copy-link"
+ style={{ background: this._buttonDown ? Colors.LIGHT_BLUE : undefined }}
+ onPointerDown={action(e => (this._buttonDown = true))}
+ onPointerUp={action(e => (this._buttonDown = false))}
+ onClick={this.copyURL}>
+ <FontAwesomeIcon title="Copy Public URL" icon="copy" size="sm" />
+ &nbsp; Copy Public URL
+ </button>
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon="times" color="black" size="lg" />
</div>
- {/* {this.linkVisible ?
- <div>
- {this.sharingUrl}
- </div> :
- (null)} */}
- {
+ {admin ? (
<div className="share-container">
<div className="share-setup">
<Select
@@ -615,9 +590,11 @@ export class SharingManager extends React.Component<{}> {
}),
}}
/>
- <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}>
- {this.sharingOptions(true)}
- </select>
+ <div className="permissions-select">
+ <select className={`permissions-dropdown-${this.permissions}`} onChange={this.handlePermissionsChange} value={this.permissions}>
+ {this.sharingOptions(true)}
+ </select>
+ </div>
<button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}>
Share
</button>
@@ -630,36 +607,41 @@ export class SharingManager extends React.Component<{}> {
<div className="acl-container">
{Doc.noviceMode ? null : (
<div className="layoutDoc-acls">
+ <input type="checkbox" onChange={action(() => (this.overrideNested = !this.overrideNested))} checked={this.overrideNested} /> <label>Override Nested </label>
<input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label>
</div>
)}
</div>
</div>
- }
+ ) : (
+ <div className="share-container">
+ <div className="acl-container">
+ <div className="layoutDoc-acls">
+ <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label>
+ </div>
+ </div>
+ </div>
+ )}
<div className="main-container">
<div className={'individual-container'}>
<div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}>
- Individuals{' '}
- {this.individualSort === 'ascending' ? (
- <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
- ) : this.individualSort === 'descending' ? (
- <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
- ) : (
- <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
- )}
+ <div className="title-individual">
+ Individuals &nbsp;
+ <FontAwesomeIcon icon={this.individualSort === 'ascending' ? 'caret-up' : this.individualSort === 'descending' ? 'caret-down' : 'caret-right'} size="xs" />
+ </div>
</div>
- <div className={'users-list'}>{userListContents}</div>
+ <div className="users-list">{userListContents}</div>
</div>
<div className={'group-container'}>
<div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
- Groups{' '}
- {this.groupSort === 'ascending' ? (
- <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
- ) : this.groupSort === 'descending' ? (
- <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
- ) : (
- <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
- )}
+ <div className="title-group">
+ Groups &nbsp;
+ <div className="group-info" onClick={action(() => GroupManager.Instance?.open())}>
+ <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} />
+ </div>
+ &nbsp;
+ <FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} size="xs" />
+ </div>
</div>
<div className={'groups-list'}>{groupListContents}</div>
</div>
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index b59af6656..9a6719ea5 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,5 +1,6 @@
import { observable, action, runInAction } from 'mobx';
import { Field } from '../../fields/Doc';
+import { RichTextField } from '../../fields/RichTextField';
import { Without } from '../../Utils';
function getBatchName(target: any, key: string | symbol): string {
@@ -96,7 +97,13 @@ export namespace UndoManager {
export function AddEvent(event: UndoEvent, value?: any): void {
if (currentBatch && batchCounter.get() && !undoing) {
- console.log(' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + (value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value)));
+ console.log(
+ ' '.slice(0, batchCounter.get()) +
+ 'UndoEvent : ' +
+ event.prop +
+ ' = ' +
+ (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value))
+ );
currentBatch.push(event);
tempEvents?.push(event);
}
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
index b95312ea0..6be2133ef 100644
--- a/src/client/views/DashboardView.scss
+++ b/src/client/views/DashboardView.scss
@@ -1,12 +1,15 @@
-@import "./global/globalCssVariables";
-
+@import './global/globalCssVariables';
.dashboard-view {
- padding: 50px;
- display: flex;
- flex-direction: row;
- width: 100%;
- position: absolute;
+ padding: 50px;
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ position: absolute;
+ height: 100%;
+ width:100%;
+ padding-right: 0px;
+ overflow: auto;
.left-menu {
display: flex;
@@ -17,24 +20,25 @@
gap: 5px;
}
- .all-dashboards {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- overflow-y: scroll;
- }
+ .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;
- }
+ padding: 3px 0;
+ &:hover {
+ font-weight: 500;
+ }
- &.selected {
- font-weight: 700;
- }
+ &.selected {
+ font-weight: 700;
+ }
}
.new-dashboard-button {
@@ -86,27 +90,37 @@
margin: 0 0px 30px 30px;
overflow: hidden;
- &:hover{
+ &:hover {
outline: solid 2px $light-blue;
- }
+ }
- .title {
- margin: 10px;
- font-weight: 500;
- }
+ .title {
+ margin: 10px;
+ font-weight: 500;
+ }
- img {
- width: auto;
- height: 80%;
- }
+ img {
+ width: auto;
+ height: 80%;
+ }
- .info {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- padding: 0px 10px;
- }
+ .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;
@@ -158,4 +172,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 dd1079d88..e574d68de 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -3,14 +3,15 @@ import { Button, ColorPicker, EditableText, FontSize, IconButton, Size, Type } f
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/FontIconBox/FontIconBox';
-import { FaPlus } from 'react-icons/fa';
enum DashboardGroup {
MyDashboards,
@@ -60,28 +60,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));
};
@@ -178,9 +180,9 @@ export class DashboardView extends React.Component {
fillWidth
/>
<Button
- text={'Shared Dashboards'}
+ text={'Shared Dashboards' + '(' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'}
active={this.selectedDashboardGroup === DashboardGroup.SharedDashboards}
- color={color}
+ color={this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'lightgreen' : color}
align={'flex-start'}
onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)}
fillWidth
@@ -188,12 +190,18 @@ export class DashboardView extends React.Component {
<Button icon={<FaPlus />} color={variant} iconPlacement="left" text="New Dashboard" type={Type.TERT} onClick={() => this.setNewDashboardName('')} />
</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)}>
+ 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='
@@ -225,6 +233,7 @@ export class DashboardView extends React.Component {
background: StrCast(Doc.UserDoc().userColor),
filter: 'opacity(0.2)'
}}/>
+ <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div>
</div>
);
})}
@@ -259,6 +268,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) => {
@@ -391,8 +405,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,
@@ -407,12 +420,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);
@@ -475,10 +485,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}`}>
+ &nbsp;
+ {shareSymbolIcon + ' ' + shareMode}
+ &nbsp;
+ {!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}
+ &nbsp;
+ </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 e84e2620f..2fa42d091 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -28,7 +28,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(
@@ -47,7 +47,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 a7164f88b..6e7498ebc 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -154,37 +154,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 889f5d5cf..b195afbd8 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -44,12 +44,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;
}
}
}
@@ -68,6 +68,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;
@@ -124,13 +177,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);
@@ -147,18 +200,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 {
@@ -179,19 +227,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 {
@@ -200,7 +238,6 @@
}
}
-
.propertiesView-field {
display: flex;
font-size: 7px;
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e86506f92..a61a14536 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -3,40 +3,40 @@ 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 { Button, EditableText, NumberDropdown, Size, Slider, Type } from 'browndash-components';
+import { Button, Colors, EditableText, NumberDropdown, Size, Slider, Type } from 'browndash-components';
import { PropertiesSection } from './PropertiesSection';
+import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
+import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
+import { KeyValueBox } from './nodes/KeyValueBox';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -303,26 +303,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>
);
}
@@ -363,6 +359,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
* @returns a row of the permissions panel
*/
sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) {
+ if (name == Doc.CurrentUserEmail) {
+ name = 'Me';
+ }
return (
<div
className="propertiesView-sharingTable-item"
@@ -376,58 +375,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() {
@@ -960,9 +1042,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
content={<>
<div className="propertiesView-buttonContainer">
{!Doc.noviceMode ? (
+ <div className="propertiesView-buttonContainer">
<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 className="propertiesView-acls-checkbox-text">Layout</div>
+ </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" />
+ </button>
+ </Tooltip> */}
</div>
) : null}
{/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}>
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 472eefa6a..a030c7052 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -281,7 +281,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 09be9dc76..00137736d 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 714619a74..b9f13b188 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -263,7 +263,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 55f62cdf2..fa9f895fd 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -222,12 +222,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);
}
};
@@ -393,7 +391,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);
@@ -456,7 +454,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);
@@ -566,7 +564,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;
};
@@ -1211,7 +1209,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 5febbe83e..e2718b52d 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';
@@ -390,7 +390,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 0b8ea1150..6b47564a4 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.headingColor,
Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().headingColor) : '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/FontIconBox/FontIconBadge.scss b/src/client/views/nodes/FontIconBox/FontIconBadge.scss
index 78f506e57..2ff5c651f 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBadge.scss
+++ b/src/client/views/nodes/FontIconBox/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/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 11f22ad5e..e911bd283 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -226,7 +226,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 1b0c6803c..b9d5ed924 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -5,7 +5,7 @@ import { Tooltip } from '@mui/material';
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';
@@ -60,7 +60,11 @@ export class TopBar extends React.Component {
return (
<div className="topbar-left">
{Doc.ActiveDashboard ? (
- <IconButton onClick={this.navigateToHome} icon={<FontAwesomeIcon icon="home" />} color={this.color} />
+ <IconButton
+ onClick={this.navigateToHome}
+ icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs.data_dashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'portrait' : 'home'} />}
+ color={this.color}
+ />
) : (
<div className="logo-container">
<img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo"></img>
@@ -115,6 +119,14 @@ export class TopBar extends React.Component {
dashView?.showContextMenu(e.clientX + 20, e.clientY + 30);
}}
/>
+ <Button
+ text={GetEffectiveAcl(Doc.ActiveDashboard) === AclAdmin ? 'Share' : 'View Original'}
+ onClick={() => {
+ SharingManager.Instance.open(undefined, Doc.ActiveDashboard);
+ }}
+ color={this.color}
+ size={Size.SMALL}
+ />
{!Doc.noviceMode && (
<IconButton
tooltip="Work on a copy of the dashboard layout"
diff --git a/src/fields/CursorField.ts b/src/fields/CursorField.ts
index a8a2859d2..46f5a8e1c 100644
--- a/src/fields/CursorField.ts
+++ b/src/fields/CursorField.ts
@@ -1,44 +1,43 @@
-import { ObjectField } from "./ObjectField";
-import { observable } from "mobx";
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, createSimpleSchema, object, date } from "serializr";
-import { OnUpdate, ToScriptString, ToString, Copy } from "./FieldSymbols";
+import { ObjectField } from './ObjectField';
+import { observable } from 'mobx';
+import { Deserializable } from '../client/util/SerializationHelper';
+import { serializable, createSimpleSchema, object, date } from 'serializr';
+import { FieldChanged, ToScriptString, ToString, Copy } from './FieldSymbols';
export type CursorPosition = {
- x: number,
- y: number
+ x: number;
+ y: number;
};
export type CursorMetadata = {
- id: string,
- identifier: string,
- timestamp: number
+ id: string;
+ identifier: string;
+ timestamp: number;
};
export type CursorData = {
- metadata: CursorMetadata,
- position: CursorPosition
+ metadata: CursorMetadata;
+ position: CursorPosition;
};
const PositionSchema = createSimpleSchema({
x: true,
- y: true
+ y: true,
});
const MetadataSchema = createSimpleSchema({
id: true,
identifier: true,
- timestamp: true
+ timestamp: true,
});
const CursorSchema = createSimpleSchema({
metadata: object(MetadataSchema),
- position: object(PositionSchema)
+ position: object(PositionSchema),
});
-@Deserializable("cursor")
+@Deserializable('cursor')
export default class CursorField extends ObjectField {
-
@serializable(object(CursorSchema))
readonly data: CursorData;
@@ -50,7 +49,7 @@ export default class CursorField extends ObjectField {
setPosition(position: CursorPosition) {
this.data.position = position;
this.data.metadata.timestamp = Date.now();
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
[Copy]() {
@@ -58,9 +57,9 @@ export default class CursorField extends ObjectField {
}
[ToScriptString]() {
- return "invalid";
+ return 'invalid';
}
[ToString]() {
- return "invalid";
+ return 'invalid';
}
-} \ No newline at end of file
+}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index f13dab68c..5a8a6e4b6 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -20,8 +20,6 @@ import {
AclEdit,
AclPrivate,
AclReadonly,
- AclSelfEdit,
- AclUnset,
Animation,
CachedUpdates,
DirectLinks,
@@ -38,11 +36,10 @@ import {
Initializing,
Self,
SelfProxy,
- Update,
UpdatingFromServer,
Width,
} from './DocSymbols';
-import { Copy, HandleUpdate, Id, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToScriptString, ToString } from './FieldSymbols';
import { InkField, InkTool } from './InkField';
import { List, ListFieldName } from './List';
import { ObjectField } from './ObjectField';
@@ -53,7 +50,7 @@ import { listSpec } from './Schema';
import { ComputedField, ScriptField } from './ScriptField';
import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types';
import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField';
-import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util';
+import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, containedFieldChangedHandler } from './util';
import JSZip = require('jszip');
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -126,21 +123,18 @@ export enum aclLevel {
unshared = 0,
viewable = 1,
augmentable = 2,
- selfEditable = 2.5,
editable = 3,
admin = 4,
}
// prettier-ignore
-export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions }> = new Map([
- [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None }],
- [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View }],
- [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment}],
- [AclSelfEdit, { level: aclLevel.selfEditable, name: SharingPermissions.SelfEdit }],
- [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit }],
- [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin }],
- [AclUnset, { level: aclLevel.unset, name: SharingPermissions.Unset }],
+export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions; image: string }> = new Map([
+ [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None, image: 'â–²' }],
+ [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View, image: '♦' }],
+ [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment, image: '⬟' }],
+ [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit, image: '⬢' }],
+ [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin, image: '⬢' }],
]);
-export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0] }]));
+export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol; image: string }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0], image: value[1].image }]));
// caches the document access permissions for the current user.
// this recursively updates all protos as well.
@@ -340,9 +334,10 @@ export class Doc extends RefField {
for (const key in value) {
const field = value[key];
field !== undefined && (this[FieldKeys][key] = true);
- if (!(field instanceof ObjectField)) continue;
- field[Parent] = this[Self];
- field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ if (field instanceof ObjectField) {
+ field[Parent] = this[Self];
+ field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field);
+ }
}
}
@@ -351,7 +346,7 @@ export class Doc extends RefField {
/// all of the raw acl's that have been set on this document. Use GetEffectiveAcl to determine the actual ACL of the doc for editing
@observable public [DocAcl]: { [key: string]: symbol } = {};
@observable public [DocCss]: number = 0; // incrementer denoting a change to CSS layout
- @observable public [DirectLinks]: Set<Doc> = new Set();
+ @observable public [DirectLinks] = new ObservableSet<Doc>();
@observable public [Animation]: Opt<Doc>;
@observable public [Highlight]: boolean = false;
static __Anim(Doc: Doc) {
@@ -363,12 +358,13 @@ export class Doc extends RefField {
private [ForceServerWrite]: boolean = false;
public [Initializing]: boolean = false;
- private [Update] = (diff: any) => {
- (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff);
- };
-
private [Self] = this;
private [SelfProxy]: any;
+ public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => {
+ if (!this[UpdatingFromServer] || this[ForceServerWrite]) {
+ DocServer.UpdateField(this[Id], serverOp);
+ }
+ };
public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any);
public [Width] = () => NumCast(this[SelfProxy]._width);
public [Height] = () => NumCast(this[SelfProxy]._height);
@@ -382,7 +378,8 @@ export class Doc extends RefField {
return self.resolvedDataDoc && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self);
}
@computed get __LAYOUT__(): Doc | undefined {
- const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null);
+ const self = this[SelfProxy];
+ const templateLayoutDoc = Cast(Doc.LayoutField(self), Doc, null);
if (templateLayoutDoc) {
let renderFieldKey: any;
const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')];
@@ -391,7 +388,7 @@ export class Doc extends RefField {
} else {
return Cast(layoutField, Doc, null);
}
- return Cast(this[SelfProxy][renderFieldKey + '-layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc;
+ return Cast(self[renderFieldKey + '-layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc;
}
return undefined;
}
@@ -480,6 +477,9 @@ export namespace Doc {
// });
// }
+ export function SetContainer(doc: Doc, container: Doc) {
+ doc.embedContainer = container;
+ }
export function RunCachedUpdate(doc: Doc, field: string) {
const update = doc[CachedUpdates][field];
if (update) {
@@ -691,7 +691,7 @@ export namespace Doc {
Doc.SetLayout(embedding, Doc.MakeEmbedding(layout));
}
embedding.createdFrom = doc;
- embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = NumCast(Doc.GetProto(doc).proto_embeddingId) + 1;
+ embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = DocListCast(Doc.GetProto(doc).proto_embeddings).length - 1;
embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`);
embedding.author = Doc.CurrentUserEmail;
@@ -746,7 +746,9 @@ export namespace Doc {
}
};
const docAtKey = doc[key];
- if (docAtKey instanceof Doc) {
+ if (key === 'author') {
+ assignKey(Doc.CurrentUserEmail);
+ } else if (docAtKey instanceof Doc) {
if (pruneDocs.includes(docAtKey)) {
// prune doc and do nothing
} else if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || ['embedContainer', 'annotationOn', 'proto'].includes(key) || ((key === 'link_anchor_1' || key === 'link_anchor_2') && doc.author === Doc.CurrentUserEmail))) {
@@ -934,6 +936,7 @@ export namespace Doc {
newLayoutDoc.rootDocument = targetDoc;
const dataDoc = Doc.GetProto(targetDoc);
newLayoutDoc.resolvedDataDoc = dataDoc;
+ newLayoutDoc['acl-Guest'] = SharingPermissions.Edit;
if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) {
dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc });
}
@@ -1019,7 +1022,6 @@ export namespace Doc {
Doc.AddDocToList(Doc.GetProto(copy)[DocData], 'proto_embeddings', copy);
}
copy.embedContainer = undefined;
- Doc.defaultAclPrivate && (copy['acl-Public'] = 'Not Shared');
if (retitle) {
copy.title = incrementTitleCopy(StrCast(copy.title));
}
@@ -1079,7 +1081,6 @@ export namespace Doc {
const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')');
target.layout_fieldKey = targetKey;
applied && (Doc.GetProto(applied).type = templateDoc.type);
- Doc.defaultAclPrivate && (applied['acl-Public'] = 'Not Shared');
return applied;
}
return undefined;
@@ -1090,7 +1091,7 @@ export namespace Doc {
target[targetKey] = new PrefetchProxy(templateDoc);
} else {
titleTarget && (Doc.GetProto(target).title = titleTarget);
- const setDoc = [AclAdmin, AclEdit].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
+ const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
setDoc[targetKey] = new PrefetchProxy(templateDoc);
}
}
diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts
index 65decc147..66d1ab094 100644
--- a/src/fields/DocSymbols.ts
+++ b/src/fields/DocSymbols.ts
@@ -1,4 +1,4 @@
-export const Update = Symbol('DocUpdate');
+export const DocUpdated = Symbol('DocUpdated');
export const Self = Symbol('DocSelf');
export const SelfProxy = Symbol('DocSelfProxy');
export const FieldKeys = Symbol('DocFieldKeys');
@@ -13,7 +13,6 @@ export const DocFields = Symbol('DocFields');
export const DocCss = Symbol('DocCss');
export const DocAcl = Symbol('DocAcl');
export const DirectLinks = Symbol('DocDirectLinks');
-export const AclUnset = Symbol('DocAclUnset');
export const AclPrivate = Symbol('DocAclOwnerOnly');
export const AclReadonly = Symbol('DocAclReadOnly');
export const AclAugment = Symbol('DocAclAugment');
diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts
index c381f14f5..0dbeb064b 100644
--- a/src/fields/FieldSymbols.ts
+++ b/src/fields/FieldSymbols.ts
@@ -1,6 +1,6 @@
export const HandleUpdate = Symbol('FieldHandleUpdate');
export const Id = Symbol('FieldId');
-export const OnUpdate = Symbol('FieldOnUpdate');
+export const FieldChanged = Symbol('FieldChanged');
export const Parent = Symbol('FieldParent');
export const Copy = Symbol('FieldCopy');
export const ToValue = Symbol('FieldToValue');
diff --git a/src/fields/List.ts b/src/fields/List.ts
index 033fa569b..183d644d3 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -3,217 +3,15 @@ import { alias, list, serializable } from 'serializr';
import { DocServer } from '../client/DocServer';
import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
import { afterDocDeserialize, autoObject, Deserializable } from '../client/util/SerializationHelper';
-import { FieldTuples, Self, SelfProxy, Update } from './DocSymbols';
import { Field } from './Doc';
-import { Copy, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols';
+import { FieldTuples, Self, SelfProxy } from './DocSymbols';
+import { Copy, FieldChanged, Parent, ToScriptString, ToString } from './FieldSymbols';
import { ObjectField } from './ObjectField';
import { ProxyField } from './Proxy';
import { RefField } from './RefField';
import { listSpec } from './Schema';
import { Cast } from './Types';
-import { deleteProperty, getter, setter, updateFunction } from './util';
-
-const listHandlers: any = {
- /// Mutator methods
- copyWithin() {
- throw new Error('copyWithin not supported yet');
- },
- fill(value: any, start?: number, end?: number) {
- if (value instanceof RefField) {
- throw new Error('fill with RefFields not supported yet');
- }
- const res = this[Self].__fieldTuples.fill(value, start, end);
- this[Update]();
- return res;
- },
- pop(): any {
- const field = toRealField(this[Self].__fieldTuples.pop());
- this[Update]();
- return field;
- },
- push: action(function (this: any, ...items: any[]) {
- items = items.map(toObjectField);
-
- const list = this[Self];
- const length = list.__fieldTuples.length;
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- //TODO Error checking to make sure parent doesn't already exist
- if (item instanceof ObjectField) {
- item[Parent] = list;
- item[OnUpdate] = updateFunction(list, i + length, item, this);
- }
- }
- const res = list.__fieldTuples.push(...items);
- this[Update]({ op: '$addToSet', items, length: length + items.length });
- return res;
- }),
- reverse() {
- const res = this[Self].__fieldTuples.reverse();
- this[Update]();
- return res;
- },
- shift() {
- const res = toRealField(this[Self].__fieldTuples.shift());
- this[Update]();
- return res;
- },
- sort(cmpFunc: any) {
- this[Self].__realFields(); // coerce retrieving entire array
- const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
- this[Update]();
- return res;
- },
- splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
- this[Self].__realFields(); // coerce retrieving entire array
- items = items.map(toObjectField);
- const list = this[Self];
- const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount);
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- //TODO Error checking to make sure parent doesn't already exist
- //TODO Need to change indices of other fields in array
- if (item instanceof ObjectField) {
- item[Parent] = list;
- item[OnUpdate] = updateFunction(list, i + start, item, this);
- }
- }
- let hintArray: { val: any; index: number }[] = [];
- for (let i = start; i < start + deleteCount; i++) {
- hintArray.push({ val: list.__fieldTuples[i], index: i });
- }
- const res = list.__fieldTuples.splice(start, deleteCount, ...items);
- // the hint object sends the starting index of the slice and the number
- // of elements to delete.
- this[Update](
- items.length === 0 && deleteCount
- ? { op: '$remFromSet', items: removed, hint: { start: start, deleteCount: deleteCount }, length: list.__fieldTuples.length }
- : items.length && !deleteCount && start === list.__fieldTuples.length
- ? { op: '$addToSet', items, length: list.__fieldTuples.length }
- : undefined
- );
- return res.map(toRealField);
- }),
- unshift(...items: any[]) {
- items = items.map(toObjectField);
- const list = this[Self];
- const length = list.__fieldTuples.length;
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- //TODO Error checking to make sure parent doesn't already exist
- //TODO Need to change indices of other fields in array
- if (item instanceof ObjectField) {
- item[Parent] = list;
- item[OnUpdate] = updateFunction(list, i, item, this);
- }
- }
- const res = this[Self].__fieldTuples.unshift(...items);
- this[Update]();
- return res;
- },
- /// Accessor methods
- concat: action(function (this: any, ...items: any[]) {
- this[Self].__realFields();
- return this[Self].__fieldTuples.map(toRealField).concat(...items);
- }),
- includes(valueToFind: any, fromIndex: number) {
- if (valueToFind instanceof RefField) {
- return this[Self].__realFields().includes(valueToFind, fromIndex);
- } else {
- return this[Self].__fieldTuples.includes(valueToFind, fromIndex);
- }
- },
- indexOf(valueToFind: any, fromIndex: number) {
- if (valueToFind instanceof RefField) {
- return this[Self].__realFields().indexOf(valueToFind, fromIndex);
- } else {
- return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex);
- }
- },
- join(separator: any) {
- this[Self].__realFields();
- return this[Self].__fieldTuples.map(toRealField).join(separator);
- },
- lastElement() {
- return this[Self].__realFields().lastElement();
- },
- lastIndexOf(valueToFind: any, fromIndex: number) {
- if (valueToFind instanceof RefField) {
- return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex);
- } else {
- return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex);
- }
- },
- slice(begin: number, end: number) {
- this[Self].__realFields();
- return this[Self].__fieldTuples.slice(begin, end).map(toRealField);
- },
-
- /// Iteration methods
- entries() {
- return this[Self].__realFields().entries();
- },
- every(callback: any, thisArg: any) {
- return this[Self].__realFields().every(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- filter(callback: any, thisArg: any) {
- return this[Self].__realFields().filter(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- find(callback: any, thisArg: any) {
- return this[Self].__realFields().find(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- findIndex(callback: any, thisArg: any) {
- return this[Self].__realFields().findIndex(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- forEach(callback: any, thisArg: any) {
- return this[Self].__realFields().forEach(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- map(callback: any, thisArg: any) {
- return this[Self].__realFields().map(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- reduce(callback: any, initialValue: any) {
- return this[Self].__realFields().reduce(callback, initialValue);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
- },
- reduceRight(callback: any, initialValue: any) {
- return this[Self].__realFields().reduceRight(callback, initialValue);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
- },
- some(callback: any, thisArg: any) {
- return this[Self].__realFields().some(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- values() {
- return this[Self].__realFields().values();
- },
- [Symbol.iterator]() {
- return this[Self].__realFields().values();
- },
-};
+import { deleteProperty, getter, setter, containedFieldChangedHandler } from './util';
function toObjectField(field: Field) {
return field instanceof RefField ? new ProxyField(field) : field;
@@ -223,38 +21,221 @@ function toRealField(field: Field) {
return field instanceof ProxyField ? field.value : field;
}
-function listGetter(target: any, prop: string | symbol, receiver: any): any {
- if (listHandlers.hasOwnProperty(prop)) {
- return listHandlers[prop];
- }
- return getter(target, prop, receiver);
-}
-
-interface ListSpliceUpdate<T> {
- type: 'splice';
- index: number;
- added: T[];
- removedCount: number;
-}
-
-interface ListIndexUpdate<T> {
- type: 'update';
- index: number;
- newValue: T;
-}
-
-type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>;
-
type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
export const ListFieldName = 'fields';
@Deserializable('list')
class ListImpl<T extends Field> extends ObjectField {
+ static listHandlers: any = {
+ /// Mutator methods
+ copyWithin() {
+ throw new Error('copyWithin not supported yet');
+ },
+ fill(value: any, start?: number, end?: number) {
+ if (value instanceof RefField) {
+ throw new Error('fill with RefFields not supported yet');
+ }
+ const res = this[Self].__fieldTuples.fill(value, start, end);
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ pop(): any {
+ const field = toRealField(this[Self].__fieldTuples.pop());
+ this[SelfProxy][FieldChanged]?.();
+ return field;
+ },
+ push: action(function (this: ListImpl<any>, ...items: any[]) {
+ items = items.map(toObjectField);
+
+ const list = this[Self];
+ const length = list.__fieldTuples.length;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], i + length, item);
+ }
+ }
+ const res = list.__fieldTuples.push(...items);
+ this[SelfProxy][FieldChanged]?.({ op: '$addToSet', items, length: length + items.length });
+ return res;
+ }),
+ reverse() {
+ const res = this[Self].__fieldTuples.reverse();
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ shift() {
+ const res = toRealField(this[Self].__fieldTuples.shift());
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ sort(cmpFunc: any) {
+ this[Self].__realFields(); // coerce retrieving entire array
+ const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
+ this[Self].__realFields(); // coerce retrieving entire array
+ items = items.map(toObjectField);
+ const list = this[Self];
+ const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount);
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[FieldChanged] = containedFieldChangedHandler(this, i + start, item);
+ }
+ }
+ let hintArray: { val: any; index: number }[] = [];
+ for (let i = start; i < start + deleteCount; i++) {
+ hintArray.push({ val: list.__fieldTuples[i], index: i });
+ }
+ const res = list.__fieldTuples.splice(start, deleteCount, ...items);
+ // the hint object sends the starting index of the slice and the number
+ // of elements to delete.
+ this[SelfProxy][FieldChanged]?.(
+ items.length === 0 && deleteCount
+ ? { op: '$remFromSet', items: removed, hint: { start, deleteCount }, length: list.__fieldTuples.length }
+ : items.length && !deleteCount && start === list.__fieldTuples.length
+ ? { op: '$addToSet', items, length: list.__fieldTuples.length }
+ : undefined
+ );
+ return res.map(toRealField);
+ }),
+ unshift(...items: any[]) {
+ items = items.map(toObjectField);
+ const list = this[Self];
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[FieldChanged] = containedFieldChangedHandler(this, i, item);
+ }
+ }
+ const res = this[Self].__fieldTuples.unshift(...items);
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ /// Accessor methods
+ concat: action(function (this: any, ...items: any[]) {
+ this[Self].__realFields();
+ return this[Self].__fieldTuples.map(toRealField).concat(...items);
+ }),
+ includes(valueToFind: any, fromIndex: number) {
+ if (valueToFind instanceof RefField) {
+ return this[Self].__realFields().includes(valueToFind, fromIndex);
+ } else {
+ return this[Self].__fieldTuples.includes(valueToFind, fromIndex);
+ }
+ },
+ indexOf(valueToFind: any, fromIndex: number) {
+ if (valueToFind instanceof RefField) {
+ return this[Self].__realFields().indexOf(valueToFind, fromIndex);
+ }
+ return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex);
+ },
+ join(separator: any) {
+ this[Self].__realFields();
+ return this[Self].__fieldTuples.map(toRealField).join(separator);
+ },
+ lastElement() {
+ return this[Self].__realFields().lastElement();
+ },
+ lastIndexOf(valueToFind: any, fromIndex: number) {
+ if (valueToFind instanceof RefField) {
+ return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex);
+ } else {
+ return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex);
+ }
+ },
+ slice(begin: number, end: number) {
+ this[Self].__realFields();
+ return this[Self].__fieldTuples.slice(begin, end).map(toRealField);
+ },
+
+ /// Iteration methods
+ entries() {
+ return this[Self].__realFields().entries();
+ },
+ every(callback: any, thisArg: any) {
+ return this[Self].__realFields().every(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ filter(callback: any, thisArg: any) {
+ return this[Self].__realFields().filter(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ find(callback: any, thisArg: any) {
+ return this[Self].__realFields().find(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ findIndex(callback: any, thisArg: any) {
+ return this[Self].__realFields().findIndex(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ forEach(callback: any, thisArg: any) {
+ return this[Self].__realFields().forEach(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ map(callback: any, thisArg: any) {
+ return this[Self].__realFields().map(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ reduce(callback: any, initialValue: any) {
+ return this[Self].__realFields().reduce(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ reduceRight(callback: any, initialValue: any) {
+ return this[Self].__realFields().reduceRight(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ some(callback: any, thisArg: any) {
+ return this[Self].__realFields().some(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ values() {
+ return this[Self].__realFields().values();
+ },
+ [Symbol.iterator]() {
+ return this[Self].__realFields().values();
+ },
+ };
+ static listGetter(target: any, prop: string | symbol, receiver: any): any {
+ if (ListImpl.listHandlers.hasOwnProperty(prop)) {
+ return ListImpl.listHandlers[prop];
+ }
+ return getter(target, prop, receiver);
+ }
constructor(fields?: T[]) {
super();
const list = new Proxy<this>(this, {
set: setter,
- get: listGetter,
+ get: ListImpl.listGetter,
ownKeys: target => Object.keys(target.__fieldTuples),
getOwnPropertyDescriptor: (target, prop) => {
if (prop in target[FieldTuples]) {
@@ -270,9 +251,9 @@ class ListImpl<T extends Field> extends ObjectField {
throw new Error("Currently properties can't be defined on documents using Object.defineProperty");
},
});
- this[SelfProxy] = list;
+ this[SelfProxy] = list as any as List<Field>; // bcz: ugh .. don't know how to convince typesecript that list is a List
if (fields) {
- (list as any).push(...fields);
+ this[SelfProxy].push(...fields);
}
return list;
}
@@ -305,10 +286,10 @@ class ListImpl<T extends Field> extends ObjectField {
private set __fieldTuples(value) {
this[FieldTuples] = value;
for (const key in value) {
- const field = value[key];
- if (field instanceof ObjectField) {
- field[Parent] = this[Self];
- field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ const item = value[key];
+ if (item instanceof ObjectField) {
+ item[Parent] = this[Self];
+ item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], Number(key), item);
}
}
}
@@ -322,16 +303,8 @@ class ListImpl<T extends Field> extends ObjectField {
// @serializable(alias("fields", list(autoObject())))
@observable
private [FieldTuples]: StoredType<T>[] = [];
-
- private [Update] = (diff: any) => {
- // console.log(diff);
- const update = this[OnUpdate];
- // update && update(diff);
- update?.(diff);
- };
-
private [Self] = this;
- private [SelfProxy]: any;
+ private [SelfProxy]: List<Field>; // also used in utils.ts even though it won't be found using find all references
[ToScriptString]() {
return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`;
diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts
index daa8a7777..b5bc2952a 100644
--- a/src/fields/ObjectField.ts
+++ b/src/fields/ObjectField.ts
@@ -1,9 +1,14 @@
-import { RefField } from "./RefField";
-import { OnUpdate, Parent, Copy, ToScriptString, ToString } from "./FieldSymbols";
-import { ScriptingGlobals } from "../client/util/ScriptingGlobals";
+import { RefField } from './RefField';
+import { FieldChanged, Parent, Copy, ToScriptString, ToString } from './FieldSymbols';
+import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
+import { Field } from './Doc';
export abstract class ObjectField {
- public [OnUpdate]?: (diff?: any) => void;
+ // prettier-ignore
+ public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set';
+ items: Field[] | undefined;
+ length: number | undefined;
+ hint?: any }, serverOp?: any) => void;
public [Parent]?: RefField | ObjectField;
abstract [Copy](): ObjectField;
@@ -17,4 +22,4 @@ export namespace ObjectField {
}
}
-ScriptingGlobals.add(ObjectField); \ No newline at end of file
+ScriptingGlobals.add(ObjectField);
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 4b1855cb0..6dde2e5aa 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -1,7 +1,7 @@
import { Deserializable } from '../client/util/SerializationHelper';
import { serializable, primitive } from 'serializr';
import { ObjectField } from './ObjectField';
-import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols';
+import { Copy, ToScriptString, ToString, FieldChanged } from './FieldSymbols';
import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView';
@@ -82,32 +82,32 @@ export class SchemaHeaderField extends ObjectField {
setHeading(heading: string) {
this.heading = heading;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setColor(color: string) {
this.color = color;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setType(type: ColumnType) {
this.type = type;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setWidth(width: number) {
this.width = width;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setDesc(desc: boolean | undefined) {
this.desc = desc;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setCollapsed(collapsed: boolean | undefined) {
this.collapsed = collapsed;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
[Copy]() {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 0f164a709..28db77c65 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,23 +1,21 @@
import { $mobx, action, observable, runInAction, trace } from 'mobx';
import { computedFn } from 'mobx-utils';
import { DocServer } from '../client/DocServer';
-import { CollectionViewType } from '../client/documents/DocumentTypes';
import { LinkManager } from '../client/util/LinkManager';
import { SerializationHelper } from '../client/util/SerializationHelper';
import { UndoManager } from '../client/util/UndoManager';
import { returnZero } from '../Utils';
-import CursorField from './CursorField';
-import { aclLevel, Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc';
-import { AclAdmin, AclEdit, AclPrivate, AclSelfEdit, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, Update, UpdatingFromServer, Width } from './DocSymbols';
-import { Id, OnUpdate, Parent, ToValue } from './FieldSymbols';
+import { aclLevel, Doc, DocListCast, Field, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc';
+import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols';
+import { FieldChanged, Id, Parent, ToValue } from './FieldSymbols';
import { List } from './List';
import { ObjectField } from './ObjectField';
import { PrefetchProxy, ProxyField } from './Proxy';
import { RefField } from './RefField';
import { RichTextField } from './RichTextField';
import { SchemaHeaderField } from './SchemaHeaderField';
-import { ComputedField, ScriptField } from './ScriptField';
-import { ScriptCast, StrCast } from './Types';
+import { ComputedField } from './ScriptField';
+import { DocCast, ScriptCast, StrCast } from './Types';
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
@@ -51,11 +49,11 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
throw new Error("Can't put the same object in multiple documents at the same time");
}
value[Parent] = receiver;
- value[OnUpdate] = updateFunction(target, prop, value, receiver);
+ value[FieldChanged] = containedFieldChangedHandler(receiver, prop, value);
}
if (curValue instanceof ObjectField) {
delete curValue[Parent];
- delete curValue[OnUpdate];
+ delete curValue[FieldChanged];
}
const effectiveAcl = GetEffectiveAcl(target);
@@ -63,8 +61,11 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
const writeMode = DocServer.getFieldWriteMode(prop as string);
const fromServer = target[UpdatingFromServer];
const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail;
- const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode !== DocServer.WriteMode.LiveReadonly;
- const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && value instanceof RichTextField)) && !DocServer.Control.isReadOnly();
+ const writeToDoc =
+ sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField);
+ const writeToServer =
+ !DocServer.Control.isReadOnly() && //
+ (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclAugment && value instanceof RichTextField));
if (writeToDoc) {
if (value === undefined) {
@@ -78,8 +79,10 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
}
if (writeToServer) {
- if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } });
- else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } });
+ // prettier-ignore
+ if (value === undefined)
+ (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $unset: { ['fields.' + prop]: '' } });
+ else (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) :value}});
if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target);
} else {
DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
@@ -105,7 +108,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
);
return true;
}
- return false;
+ return true;
});
let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl;
@@ -128,14 +131,18 @@ export function denormalizeEmail(email: string) {
/**
* Copies parent's acl fields to the child
*/
-export function inheritParentAcls(parent: Doc, child: Doc) {
- return;
- // const dataDoc = parent[DataSym];
- // for (const key of Object.keys(dataDoc)) {
- // // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private.
- // const permission = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : dataDoc[key];
- // key.startsWith('acl') && distributeAcls(key, permission, child);
- // }
+export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) {
+ [...Object.keys(parent), ...(Doc.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])]
+ .filter(key => key.startsWith('acl'))
+ .forEach(key => {
+ // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private.
+ // const permission: string = key === 'acl-guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key];
+ const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl-Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl;
+ if (parAcl) {
+ const sharePermission = HierarchyMapping.get(parAcl)?.name;
+ sharePermission && distributeAcls(key === 'acl-Owner' ? `acl-${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly);
+ }
+ });
}
/**
@@ -154,13 +161,11 @@ export function inheritParentAcls(parent: Doc, child: Doc) {
* Unset: Remove a sharing permission (eg., used )
*/
export enum SharingPermissions {
- Unset = 'None',
Admin = 'Admin',
Edit = 'Edit',
- SelfEdit = 'Self Edit',
Augment = 'Augment',
View = 'View',
- None = 'Not Shared',
+ None = 'Not-Shared',
}
// return acl from cache or cache the acl and return.
@@ -173,11 +178,11 @@ const getEffectiveAclCache = computedFn(function (target: any, user?: string) {
*/
export function GetEffectiveAcl(target: any, user?: string): symbol {
if (!target) return AclPrivate;
- if (target[UpdatingFromServer]) return AclAdmin;
+ if (target[UpdatingFromServer] || Doc.CurrentUserEmail === 'guest') return AclAdmin;
return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable)
}
-function getPropAcl(target: any, prop: string | symbol | number) {
+export function GetPropAcl(target: any, prop: string | symbol | number) {
if (typeof prop === 'symbol' || target[UpdatingFromServer]) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable
return GetEffectiveAcl(target);
@@ -204,18 +209,13 @@ function getEffectiveAcl(target: any, user?: string): symbol {
// there are issues with storing fields with . in the name, so they are replaced with _ during creation
// as a result we need to restore them again during this comparison.
const entity = denormalizeEmail(key.substring(4)); // an individual or a group
- if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) {
- if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') {
+ if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') {
+ if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) {
effectiveAcl = value as symbol;
}
}
}
- // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document
- //const override = targetAcls['acl-Override'];
- // if (override !== AclUnset && override !== undefined) effectiveAcl = override;
-
- // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl)
return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl;
}
// authored documents are private until an ACL is set.
@@ -223,73 +223,72 @@ function getEffectiveAcl(target: any, user?: string): symbol {
if (targetAuthor && targetAuthor !== userChecked) return AclPrivate;
return AclAdmin;
}
+
/**
* Recursively distributes the access right for a user across the children of a document and its annotations.
* @param key the key storing the access right (e.g. acl-groupname)
* @param acl the access right being stored (e.g. "Can Edit")
* @param target the document on which this access right is being set
- * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection)
- * inheritingFromCollection is not currently being used but could be used if acl assignment defaults change
+ * @param visited list of Doc's already distributed to.
+ * @param allowUpgrade whether permissions can be made less restrictive
+ * @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well)
*/
-export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) {
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean, layoutOnly = false) {
+ const selfKey = `acl-${Doc.CurrentUserEmailNormalized}`;
if (!visited) visited = [] as Doc[];
- if (!target || visited.includes(target)) return;
-
- if ((target._type_collection === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) {
- target[key] = acl;
- if (target !== Doc.GetProto(target)) {
- //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???)
- updateCachedAcls(target);
- }
- return;
- }
+ if (!target || visited.includes(target) || key === selfKey) return;
visited.push(target);
- let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym])
- // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive
- if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) {
- target[key] = acl;
- layoutDocChanged = true;
-
- if (isDashboard) {
- DocListCastAsync(target[Doc.LayoutFieldKey(target)]).then(docs => {
- docs?.forEach(d => distributeAcls(key, acl, d, inheritingFromCollection, visited));
- });
- }
- }
-
let dataDocChanged = false;
const dataDoc = target[DocData];
- if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) {
- if (GetEffectiveAcl(dataDoc) === AclAdmin) {
- dataDoc[key] = acl;
- dataDocChanged = true;
- }
+ const curVal = ReverseHierarchyMap.get(StrCast(dataDoc[key]))?.level ?? 0;
+ const aclVal = ReverseHierarchyMap.get(acl)?.level ?? 0;
+ if (!layoutOnly && dataDoc && (allowUpgrade !== false || !dataDoc[key] || curVal > aclVal)) {
+ // propagate ACLs to links, children, and annotations
- // maps over the links of the document
- LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
+ LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, visited, allowUpgrade ? true : false));
- // maps over the children of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => {
- distributeAcls(key, acl, d, inheritingFromCollection, visited);
- distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited);
+ distributeAcls(key, acl, d, visited, allowUpgrade ? true : false);
+ d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false);
});
- // maps over the annotations of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => {
- distributeAcls(key, acl, d, inheritingFromCollection, visited);
- distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited);
+ distributeAcls(key, acl, d, visited, allowUpgrade ? true : false);
+ d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false);
});
+
+ Object.keys(target) // share expanded layout templates (eg, for presElementBox'es )
+ .filter(lkey => lkey.includes('layout[') && DocCast(target[lkey]))
+ .map(lkey => {
+ distributeAcls(key, acl, DocCast(target[lkey]), visited, allowUpgrade ? true : false);
+ });
+
+ if (GetEffectiveAcl(dataDoc) === AclAdmin) {
+ dataDoc[key] = acl;
+ dataDocChanged = true;
+ }
+ }
+
+ let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym])
+ // if it is inheriting from a collection, it only inherits if A) allowUpgrade is set B) the key doesn't already exist or c) the right being inherited is more restrictive
+ if (GetEffectiveAcl(target) === AclAdmin && (allowUpgrade || !Doc.GetT(target, key, 'boolean', true) || ReverseHierarchyMap.get(StrCast(target[key]))!.level > aclVal)) {
+ target[key] = acl;
+ layoutDocChanged = true;
+ if (dataDoc[key] === undefined) dataDoc[key] = acl;
}
layoutDocChanged && updateCachedAcls(target); // updates target[AclSym] when changes to acls have been made
dataDocChanged && updateCachedAcls(dataDoc);
}
+//
+// target should be either a Doc or ListImpl. receiver should be a Proxy<Doc> Or List.
+//
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
- const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : getPropAcl(target, prop);
- if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true;
+ const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop);
+ if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true;
// if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
@@ -356,86 +355,104 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
return true;
}
-export function updateFunction(target: any, prop: any, value: any, receiver: any) {
- let lastValue = ObjectField.MakeCopy(value);
- return (diff?: any) => {
- const op =
- diff?.op === '$addToSet'
- ? { $addToSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
- : diff?.op === '$remFromSet'
- ? { $remFromSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)), hint: diff.hint } }
- : { $set: { ['fields.' + prop]: SerializationHelper.Serialize(value) } };
- !op.$set && ((op as any).length = diff.length);
- const prevValue = ObjectField.MakeCopy(lastValue as List<any>);
- lastValue = ObjectField.MakeCopy(value);
- const newValue = ObjectField.MakeCopy(value);
-
- if (!(value instanceof CursorField) && !value?.some?.((v: any) => v instanceof CursorField)) {
- !receiver[UpdatingFromServer] &&
+// this function creates a function that can be used to setup Undo for whenever an ObjectField changes.
+// the idea is that the Doc field setter can only setup undo at the granularity of an entire field and won't even be called if
+// just a part of a field (eg. field within an ObjectField) changes. This function returns a function that can be called
+// whenever an internal ObjectField field changes. It should be passed a 'diff' specification describing the change. Currently,
+// List's are the only true ObjectFields that can be partially modified (ignoring SchemaHeaderFields which should go away).
+// The 'diff' specification that a list can send is limited to indicating that something was added, removed, or that the list contents
+// were replaced. Based on this specification, an Undo event is setup that will save enough information about the ObjectField to be
+// able to undo and redo the partial change.
+//
+export function containedFieldChangedHandler(container: List<Field> | Doc, prop: string | number, liveContainedField: ObjectField) {
+ let lastValue: FieldResult = liveContainedField instanceof ObjectField ? ObjectField.MakeCopy(liveContainedField) : liveContainedField;
+ return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => {
+ const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: Field) => SerializationHelper.Serialize(item)) });
+ // prettier-ignore
+ const serverOp = diff?.op === '$addToSet'
+ ? { $addToSet: { ['fields.' + prop]: serializeItems() }, length: diff.length }
+ : diff?.op === '$remFromSet'
+ ? { $remFromSet: { ['fields.' + prop]: serializeItems(), hint: diff.hint}, length: diff.length }
+ : { $set: { ['fields.' + prop]: liveContainedField ? SerializationHelper.Serialize(liveContainedField) : undefined } };
+
+ if (!(container instanceof Doc) || !container[UpdatingFromServer]) {
+ const prevValue = ObjectField.MakeCopy(lastValue as List<any>);
+ lastValue = ObjectField.MakeCopy(liveContainedField);
+ const newValue = ObjectField.MakeCopy(liveContainedField);
+ if (diff?.op === '$addToSet') {
UndoManager.AddEvent(
- diff?.op === '$addToSet'
- ? {
- redo: () => {
- console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
- receiver[prop].push(...diff.items.map((item: any) => item.value ?? item));
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- },
- undo: action(() => {
- // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo
- diff.items.forEach((item: any) => {
- if (item instanceof SchemaHeaderField) {
- const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
- ind !== -1 && receiver[prop].splice(ind, 1);
- } else {
- const ind = receiver[prop].indexOf(item.value ?? item);
- ind !== -1 && receiver[prop].splice(ind, 1);
- }
- });
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- }),
- prop: 'add ' + diff.items.length + ' items to list',
- }
- : diff?.op === '$remFromSet'
- ? {
- redo: action(() => {
- console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
- diff.items.forEach((item: any) => {
- const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item);
- ind !== -1 && receiver[prop].splice(ind, 1);
- });
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- }),
- undo: () => {
- // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo
- diff.items.forEach((item: any) => {
- if (item instanceof SchemaHeaderField) {
- const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
- ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item);
- } else {
- const ind = (prevValue as List<any>).indexOf(item.value ?? item);
- ind !== -1 && receiver[prop].indexOf(item.value ?? item) === -1 && receiver[prop].splice(ind, 0, item);
- }
- });
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- },
- prop: 'remove ' + diff.items.length + ' items from list',
- }
- : {
- redo: () => {
- console.log('redo list: ' + prop, receiver[prop]); // bcz: uncomment to log undo
- receiver[prop] = ObjectField.MakeCopy(newValue as List<any>);
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- },
- undo: () => {
- // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo
- receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>);
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- },
- prop: 'assign list',
- },
+ {
+ redo: () => {
+ //console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
+ (container as any)[prop as any]?.push(...(diff.items || [])?.map((item: any) => item.value ?? item));
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ },
+ undo: action(() => {
+ // console.log('undo $add: ' + prop, diff.items); // bcz: uncomment to log undo
+ diff.items?.forEach((item: any) => {
+ const ind =
+ item instanceof SchemaHeaderField //
+ ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading)
+ : (container as any)[prop as any]?.indexOf(item.value ?? item);
+ ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1);
+ });
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ }),
+ prop: 'add ' + diff.items?.length + ' items to list',
+ },
+ diff?.items
+ );
+ } else if (diff?.op === '$remFromSet') {
+ UndoManager.AddEvent(
+ {
+ redo: action(() => {
+ // console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
+ diff.items?.forEach((item: any) => {
+ const ind =
+ item instanceof SchemaHeaderField //
+ ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading)
+ : (container as any)[prop as any]?.indexOf(item.value ?? item);
+ ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1);
+ });
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ }),
+ undo: () => {
+ // console.log('undo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
+ diff.items?.forEach((item: any) => {
+ if (item instanceof SchemaHeaderField) {
+ const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
+ ind !== -1 && (container as any)[prop as any].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && (container as any)[prop as any].splice(ind, 0, item);
+ } else {
+ const ind = (prevValue as List<any>).indexOf(item.value ?? item);
+ ind !== -1 && (container as any)[prop as any].indexOf(item.value ?? item) === -1 && (container as any)[prop as any].splice(ind, 0, item);
+ }
+ });
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ },
+ prop: 'remove ' + diff.items?.length + ' items from list',
+ },
diff?.items
);
+ } else {
+ const setFieldVal = (val: Field | undefined) => (container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as Field));
+ UndoManager.AddEvent(
+ {
+ redo: () => {
+ // console.log('redo list: ' + prop, fieldVal()); // bcz: uncomment to log undo
+ setFieldVal(newValue instanceof ObjectField ? ObjectField.MakeCopy(newValue) : undefined);
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ },
+ undo: () => {
+ // console.log('undo list: ' + prop, fieldVal()); // bcz: uncomment to log undo
+ setFieldVal(prevValue instanceof ObjectField ? ObjectField.MakeCopy(prevValue) : undefined);
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ },
+ prop: 'set list field',
+ },
+ diff?.items
+ );
+ }
}
- target[Update](op);
+ container[FieldChanged]?.(undefined, serverOp);
};
}