diff options
author | bobzel <zzzman@gmail.com> | 2020-08-19 00:19:41 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2020-08-19 00:19:41 -0400 |
commit | a56af1c509199ca26a227132dd130139dc32988d (patch) | |
tree | 0db0d110f449898283cb26eaaea5a1fe1ea144e1 | |
parent | c5e7890b5055521bf030eff8db99ff189b3f9eb2 (diff) | |
parent | 677d73e55fc934fac0fd1170a399359e131fef2f (diff) |
Merge branch 'master' into schema_search
39 files changed, 406 insertions, 282 deletions
diff --git a/solr-8.3.1/server/solr/dash/conf/schema.xml b/solr-8.3.1/server/solr/dash/conf/schema.xml index 4f6ae57b2..74c4b494c 100644 --- a/solr-8.3.1/server/solr/dash/conf/schema.xml +++ b/solr-8.3.1/server/solr/dash/conf/schema.xml @@ -51,7 +51,7 @@ <field name="proto" type="string" indexed="true" stored="true" uninvertible="false" /> <!-- <dynamicField name="*_s" type="string" indexed="true" stored="false" uninvertible="false" docValues="true"/> --> - <dynamicField name="*_t" type="text" indexed="true" stored="true" uninvertible="false" docValues="false"/> + <dynamicField name="*_t" type="text" indexed="true" stored="true" uninvertible="true" docValues="false"/> <dynamicField name="*_n" type="pdouble" indexed="true" stored="true" uninvertible="false" docValues="false"/> <dynamicField name="*_d" type="pdate" indexed="true" stored="true" uninvertible="false" docValues="true"/> <dynamicField name="*_l" type="list" indexed="true" stored="true" uninvertible="false" docValues="false" multiValued="true"/> diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d612f7b5c..f07b718ba 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -20,7 +20,7 @@ import { dropActionType } from "../util/DragManager"; import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox"; import { LinkManager } from "../util/LinkManager"; import { Scripting } from "../util/Scripting"; -import { UndoManager } from "../util/UndoManager"; +import { UndoManager, undoBatch } from "../util/UndoManager"; import { DocumentType } from "./DocumentTypes"; import { SearchBox } from "../views/search/SearchBox"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; @@ -577,7 +577,7 @@ export namespace Docs { // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. dataDoc[fieldKey + "-annotations"] = new List<Doc>(); - dataDoc.aliases = new List<Doc>(); + dataDoc.aliases = new List<Doc>([viewDoc]); proto.links = ComputedField.MakeFunction("links(self)"); @@ -1042,7 +1042,7 @@ export namespace DocUtils { description: "Add Note ...", subitems: DocListCast((Doc.UserDoc()["template-notes"] as Doc).data).map((note, i) => ({ description: ":" + StrCast(note.title), - event: (args: { x: number, y: number }) => { + event: undoBatch((args: { x: number, y: number }) => { const textDoc = Docs.Create.TextDocument("", { _width: 200, x, y, _autoHeight: note._autoHeight !== false, title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1) @@ -1050,7 +1050,7 @@ export namespace DocUtils { textDoc.layoutKey = "layout_" + note.title; textDoc[textDoc.layoutKey] = note; docTextAdder(textDoc); - }, + }), icon: "eye" })) as ContextMenuProps[], icon: "eye" @@ -1059,7 +1059,7 @@ export namespace DocUtils { description: "Add Template Doc ...", subitems: DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc).map((dragDoc, i) => ({ description: ":" + StrCast(dragDoc.title), - event: (args: { x: number, y: number }) => { + event: undoBatch((args: { x: number, y: number }) => { const newDoc = Doc.ApplyTemplate(dragDoc); if (newDoc) { newDoc.author = Doc.CurrentUserEmail; @@ -1067,7 +1067,7 @@ export namespace DocUtils { newDoc.y = y; docAdder(newDoc); } - }, + }), icon: "eye" })) as ContextMenuProps[], icon: "eye" diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 647efeec0..4385b164b 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -810,7 +810,6 @@ export class CurrentUserUtils { if (doc.sidebar === undefined) { const sidebarContainer = new Doc(); sidebarContainer._chromeStatus = "disabled"; - sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()"); sidebarContainer.system = true; doc.sidebar = new PrefetchProxy(sidebarContainer); } @@ -868,7 +867,7 @@ export class CurrentUserUtils { // Sharing sidebar is where shared documents are contained static setupSharingSidebar(doc: Doc) { if (doc["sidebar-sharing"] === undefined) { - doc["sidebar-sharing"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Shared Documents", childDropAction: "alias", system: true })); + doc["sidebar-sharing"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Shared Documents", childDropAction: "alias", system: true, _yMargin: 30, _showTitle: "title", ignoreClick: true, lockedPosition: true })); } } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 9ede94e4b..04e937878 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -34,7 +34,7 @@ export namespace Hypothesis { const results: Doc[] = []; await SearchUtil.Search("web", true).then(action(async (res: SearchUtil.DocSearchResult) => { - const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); + const docs = res.docs; const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data ); diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index ae3b3e064..13c4fac25 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -140,7 +140,7 @@ export namespace InteractionUtils { export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) { + dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) { let pts: { X: number; Y: number; }[] = []; if (shape) { //if any of the shape are true pts = makePolygon(shape, points); @@ -182,7 +182,7 @@ export namespace InteractionUtils { const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${(pt.X - left - width / 2) * scalex + width / 2}, ${(pt.Y - top - width / 2) * scaley + width / 2} `, ""); - const dashArray = String(Number(width) * Number(dash)); + const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined; const defGuid = Utils.GenerateGuid(); const arrowDim = Math.max(0.5, 8 / Math.log(Math.max(2, strokeWidth))); diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index b63fc8991..ce96ab67b 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -28,7 +28,9 @@ export namespace SearchUtil { start?: number; rows?: number; fq?: string; + sort?: string; allowAliases?: boolean; + onlyAliases?: boolean; "facet"?: string; "facet.field"?: string; } @@ -37,8 +39,11 @@ export namespace SearchUtil { export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) { query = query || "*"; //If we just have a filter query, search for * as the query const rpquery = Utils.prepend("/dashsearch"); - const replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}type_t:${arg}`); - const gotten = await rp.get(rpquery, { qs: { ...options, sort: "lastModified_d desc", q: replacedQuery } }); + let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}type_t:${arg}`); + if (options.onlyAliases) { + replacedQuery = `{!join from=id to=proto_i}DEFAULT:${replacedQuery}`; + } + const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } }); const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten); if (!returnDocs) { return result; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 113278593..35b82cc30 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,10 +1,9 @@ import { observable, action, runInAction, ObservableMap } from "mobx"; -import { Doc } from "../../fields/Doc"; +import { Doc, Opt } from "../../fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { computedFn } from "mobx-utils"; import { List } from "../../fields/List"; -import { Scripting } from "./Scripting"; -import { DocumentManager } from "./DocumentManager"; +import { CollectionSchemaView } from "../views/collections/CollectionSchemaView"; export namespace SelectionManager { @@ -12,8 +11,15 @@ export namespace SelectionManager { @observable IsDragging: boolean = false; SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); + @observable SelectedSchemaDocument: Doc | undefined; + @observable SelectedSchemaCollection: CollectionSchemaView | undefined; @action + SelectSchemaDoc(collectionView: Opt<CollectionSchemaView>, doc: Opt<Doc>) { + manager.SelectedSchemaDocument = doc; + manager.SelectedSchemaCollection = collectionView; + } + @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it @@ -26,6 +32,8 @@ export namespace SelectionManager { docView.props.whenActiveChanged(true); } else if (!ctrlPressed && Array.from(manager.SelectedDocuments.entries()).length > 1) { Array.from(manager.SelectedDocuments.keys()).map(dv => dv !== docView && dv.props.whenActiveChanged(false)); + manager.SelectedSchemaDocument = undefined; + manager.SelectedSchemaCollection = undefined; manager.SelectedDocuments.clear(); manager.SelectedDocuments.set(docView, true); } @@ -42,7 +50,8 @@ export namespace SelectionManager { } @action DeselectAll(): void { - + manager.SelectedSchemaCollection = undefined; + manager.SelectedSchemaDocument = undefined; Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false)); manager.SelectedDocuments.clear(); Doc.UserDoc().activeSelection = new List<Doc>([]); @@ -57,6 +66,9 @@ export namespace SelectionManager { export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { manager.SelectDoc(docView, ctrlPressed); } + export function SelectSchemaDoc(colSchema: Opt<CollectionSchemaView>, document: Opt<Doc>): void { + manager.SelectSchemaDoc(colSchema, document); + } // computed functions, such as used in IsSelected generate errors if they're called outside of a // reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature @@ -84,4 +96,10 @@ export namespace SelectionManager { export function SelectedDocuments(): Array<DocumentView> { return Array.from(manager.SelectedDocuments.keys()); } + export function SelectedSchemaDoc(): Doc | undefined { + return manager.SelectedSchemaDocument; + } + export function SelectedSchemaCollection(): CollectionSchemaView | undefined { + return manager.SelectedSchemaCollection; + } }
\ No newline at end of file diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 7912db74d..42c300712 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -62,6 +62,7 @@ input { height: 10px; + cursor: pointer; } label { @@ -69,11 +70,29 @@ font-style: italic; } } + + .layoutDoc-acls { + display: flex; + flex-direction: column; + float: right; + margin-right: 12; + margin-top: -15; + align-items: center; + + label { + font-weight: normal; + font-style: italic; + } + + input { + cursor: pointer; + } + } } .main-container { display: flex; - margin-top: -10px; + margin-top: -25px; .individual-container, .group-container { diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 48a3c023f..b9918e900 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,7 +1,7 @@ import { observable, runInAction, action } from "mobx"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; -import { Doc, Opt, AclAdmin, AclPrivate, DocListCast } from "../../fields/Doc"; +import { Doc, Opt, AclAdmin, AclPrivate, DocListCast, DataSym } from "../../fields/Doc"; import { DocServer } from "../DocServer"; import { Cast, StrCast } from "../../fields/Types"; import * as RequestPromise from "request-promise"; @@ -25,7 +25,6 @@ import { library } from "@fortawesome/fontawesome-svg-core"; library.add(fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown); - export interface User { email: string; userDocumentId: string; @@ -47,6 +46,8 @@ interface GroupedOptions { const indType = "!indType/"; const groupType = "!groupType/"; +const storage = "data"; + /** * A user who also has a notificationDoc. */ @@ -55,7 +56,6 @@ interface ValidatedUser { notificationDoc: Doc; } -const storage = "data"; @observer export default class SharingManager extends React.Component<{}> { @@ -75,21 +75,22 @@ export default class SharingManager extends React.Component<{}> { // 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; + private populating: boolean = false; // whether the list of users is populating or not + @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used // private get linkVisible() { // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; // } - public open = (target: DocumentView) => { + public open = (target?: DocumentView, target_doc?: Doc) => { runInAction(() => this.users = []); // SelectionManager.DeselectAll(); this.populateUsers(); runInAction(() => { this.targetDocView = target; - this.targetDoc = target.props.Document; + this.targetDoc = target_doc || target?.props.Document; DictationOverlay.Instance.hasActiveModal = true; - this.isOpen = true; + this.isOpen = this.targetDoc !== undefined; this.permissions = SharingPermissions.Edit; }); this.targetDoc!.author === Doc.CurrentUserEmail && !this.targetDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, this.targetDoc!); @@ -155,16 +156,17 @@ export default class SharingManager extends React.Component<{}> { const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); const target = targetDoc || this.targetDoc!; - const ACL = `ACL-${StrCast(group.groupName)}`; + const key = StrCast(group.groupName).replace(".", "_"); + const ACL = `ACL-${key}`; - target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); + GetEffectiveAcl(target) === AclAdmin && distributeAcls(ACL, permission as SharingPermissions, target); // if documents have been shared, add the target to that list if it doesn't already exist, otherwise create a new list with the target group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(target) : group.docsShared = new List<Doc>([target]); - users.forEach(({ notificationDoc }) => { + users.forEach(({ user, notificationDoc }) => { if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added - else Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists + else GetEffectiveAcl(target, undefined, user.email) === AclPrivate && Doc.IndexOf((target.aliasOf as Doc || target), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (target.aliasOf as Doc || target)); // remove the target from the list if it already exists }); } @@ -175,7 +177,6 @@ export default class SharingManager extends React.Component<{}> { */ shareWithAddedMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; - if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); } @@ -183,9 +184,12 @@ export default class SharingManager extends React.Component<{}> { * Called from the properties sidebar to change permissions of a user. */ shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, target: Doc) => { - const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith)); - if (user) this.setInternalSharing(user, permission, target); - else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, target); + if (shareWith !== "Public") { + const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith)); + if (user) this.setInternalSharing(user, permission, target); + else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, target); + } + else if (GetEffectiveAcl(target) === AclAdmin) distributeAcls("ACL-Public", permission, target); } /** @@ -231,14 +235,11 @@ export default class SharingManager extends React.Component<{}> { const key = user.email.replace('.', '_'); const ACL = `ACL-${key}`; - target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); + GetEffectiveAcl(target) === AclAdmin && distributeAcls(ACL, permission as SharingPermissions, target); + + if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); + else GetEffectiveAcl(target, undefined, user.email) === AclPrivate && Doc.IndexOf((target.aliasOf as Doc || target), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (target.aliasOf as Doc || target)); - if (permission !== SharingPermissions.None) { - Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); - } - else { - Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); - } } @@ -372,8 +373,8 @@ export default class SharingManager extends React.Component<{}> { * @returns the main interface of the SharingManager. */ private get sharingInterface() { - const groupList = GroupManager.Instance?.getAllGroups() || []; + const groupList = GroupManager.Instance?.getAllGroups() || []; const sortedUsers = this.users.slice().sort(this.sortUsers) .map(({ user: { email } }) => ({ label: email, value: indType + email })); const sortedGroups = groupList.slice().sort(this.sortGroups) @@ -406,17 +407,19 @@ export default class SharingManager extends React.Component<{}> { } } - const users = this.individualSort === "ascending" ? this.users.sort(this.sortUsers) : this.individualSort === "descending" ? this.users.sort(this.sortUsers).reverse() : this.users; - const groups = this.groupSort === "ascending" ? groupList.sort(this.sortGroups) : this.groupSort === "descending" ? groupList.sort(this.sortGroups).reverse() : groupList; + 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; + + const targetDoc = this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]; - const effectiveAcl = this.targetDoc ? GetEffectiveAcl(this.targetDoc) : AclPrivate; + const effectiveAcl = targetDoc ? GetEffectiveAcl(targetDoc) : AclPrivate; // the list of users shared with const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => { const userKey = user.email.replace('.', '_'); - const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`]); + const permissions = StrCast(targetDoc?.[`ACL-${userKey}`]); - return !permissions || user.email === this.targetDoc?.author ? null : ( + return !permissions || user.email === targetDoc?.author ? (null) : ( <div key={userKey} className={"container"} @@ -448,7 +451,7 @@ export default class SharingManager extends React.Component<{}> { key={"owner"} className={"container"} > - <span className={"padding"}>{this.targetDoc?.author === Doc.CurrentUserEmail ? "Me" : this.targetDoc?.author}</span> + <span className={"padding"}>{targetDoc?.author === Doc.CurrentUserEmail ? "Me" : targetDoc?.author}</span> <div className="edit-actions"> <div className={"permissions-dropdown"}> Owner @@ -456,7 +459,7 @@ export default class SharingManager extends React.Component<{}> { </div> </div> ), - this.targetDoc?.author !== Doc.CurrentUserEmail ? + targetDoc?.author !== Doc.CurrentUserEmail ? ( <div key={"me"} @@ -465,7 +468,7 @@ export default class SharingManager extends React.Component<{}> { <span className={"padding"}>Me</span> <div className="edit-actions"> <div className={"permissions-dropdown"}> - {this.targetDoc?.[`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]} + {targetDoc?.[`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]} </div> </div> </div> @@ -474,7 +477,7 @@ export default class SharingManager extends React.Component<{}> { // the list of groups shared with const groupListContents = groups.map(group => { - const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`]); + const permissions = StrCast(targetDoc?.[`ACL-${StrCast(group.groupName)}`]); return !permissions ? null : ( <div @@ -540,7 +543,7 @@ export default class SharingManager extends React.Component<{}> { </div> <div className={"hr-substitute"} /> */} <div className="sharing-contents"> - <p className={"share-title"}><b>Share </b>{this.focusOn(StrCast(this.targetDoc?.title, "this document"))}</p> + <p className={"share-title"}><b>Share </b>{this.focusOn(StrCast(targetDoc?.title, "this document"))}</p> <div className={"close-button"} onClick={this.close}> <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> </div> @@ -571,6 +574,9 @@ export default class SharingManager extends React.Component<{}> { <input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label> <input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label> </div> + <div className="layoutDoc-acls"> + <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> + </div> </div> } <div className="main-container"> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 8c8bb6fde..23ce71c4f 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -135,7 +135,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T toRemove.forEach(doc => { Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey + "-annotations", doc); recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); - doc.deleted = true; }); return true; } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 8748b1880..7effc4aa0 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -199,7 +199,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}> <div className="documentButtonBar-linker" style={{ backgroundColor: isPinned ? "white" : "", color: isPinned ? "black" : "white", border: isPinned ? "black 1px solid " : "" }} - onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> + onClick={e => this.props.views().map(view => view && DockedFrameRenderer.PinDoc(view.props.Document, isPinned))}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div></Tooltip>; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index e546ca858..0cc492ee9 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,34 +1,30 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; +import { faAngleDoubleLeft, faAngleDoubleRight, faAngleLeft, faAngleRight, faArrowAltCircleDown, faArrowAltCircleUp, faCaretUp, faCheckCircle, faCloudUploadAlt, faExternalLinkAlt, faFilePdf, faFilm, faImage, faLink, faObjectGroup, faPause, faShare, faStickyNote, faStopCircle, faSyncAlt, faTag, faTextHeight, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, reaction, runInAction, get } from "mobx"; +import { Tooltip } from '@material-ui/core'; +import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym, Field, WidthSym, HeightSym, AclEdit, AclAdmin } from "../../fields/Doc"; +import { AclAdmin, AclEdit, DataSym, Doc, Field } from "../../fields/Doc"; import { Document } from '../../fields/documentSchemas'; +import { HtmlField } from '../../fields/HtmlField'; +import { InkField } from "../../fields/InkField"; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, StrCast, NumCast } from "../../fields/Types"; -import { Utils, setupMoveUpEvents, emptyFunction, returnFalse, simulateMouseClick } from "../../Utils"; +import { Cast, NumCast } from "../../fields/Types"; +import { GetEffectiveAcl } from '../../fields/util'; +import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from "../../Utils"; import { DocUtils } from "../documents/Documents"; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; +import { SnappingManager } from '../util/SnappingManager'; import { undoBatch, UndoManager } from "../util/UndoManager"; +import { CollectionDockingView } from './collections/CollectionDockingView'; +import FormatShapePane from './collections/collectionFreeForm/FormatShapePane'; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); import e = require('express'); -import { CollectionDockingView } from './collections/CollectionDockingView'; -import { SnappingManager } from '../util/SnappingManager'; -import { HtmlField } from '../../fields/HtmlField'; -import { InkField } from "../../fields/InkField"; -import { Tooltip } from '@material-ui/core'; -import { GetEffectiveAcl } from '../../fields/util'; -import { DocumentIcon } from './nodes/DocumentIcon'; -import { render } from 'react-dom'; -import { createLessThan } from 'typescript'; -import FormatShapePane from './collections/collectionFreeForm/FormatShapePane'; -import { PropertiesView } from './collections/collectionFreeForm/PropertiesView'; library.add(faCaretUp); library.add(faObjectGroup); @@ -364,7 +360,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY; this.Interacting = true; this._resizeUndo = UndoManager.StartBatch("DocDecs resize"); - SelectionManager.SelectedDocuments()[0].props.setupDragLines?.(); + SelectionManager.SelectedDocuments()[0].props.setupDragLines?.(e.ctrlKey || e.shiftKey); } this._snapX = e.pageX; this._snapY = e.pageY; @@ -603,7 +599,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const darkScheme = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimgray" : undefined; const bounds = this.Bounds; const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; - if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } const canDelete = SelectionManager.SelectedDocuments().some(docView => { diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 76e786257..c25ebbf11 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -613,10 +613,10 @@ export default class GestureOverlay extends Touchable { } } // if we're not drawing in a toolglass try to recognize as gesture - else { + else { // need to decide when to turn gestures back on const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize(new Array(points)); let actionPerformed = false; - if (result && result.Score > 0.7) { + if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) { switch (result.Name) { case GestureUtils.Gestures.Box: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Box); break; case GestureUtils.Gestures.StartBracket: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.StartBracket); break; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 41311ed86..3376fcd97 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -103,11 +103,15 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const strokeWidth = Number(this.layoutDoc.strokeWidth); const xs = data.map(p => p.X); const ys = data.map(p => p.Y); - const left = Math.min(...xs) - strokeWidth / 2; - const top = Math.min(...ys) - strokeWidth / 2; - const right = Math.max(...xs) + strokeWidth / 2; - const bottom = Math.max(...ys) + strokeWidth / 2; - const width = Math.max(right - left); + const lineTop = Math.min(...ys); + const lineBot = Math.max(...ys); + const lineLft = Math.min(...xs); + const lineRgt = Math.max(...xs); + const left = lineLft - strokeWidth / 2; + const top = lineTop - strokeWidth / 2; + const right = lineRgt + strokeWidth / 2; + const bottom = lineBot + strokeWidth / 2; + const width = Math.max(1, right - left); const height = Math.max(1, bottom - top); const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); @@ -116,12 +120,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), - StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); const hpoints = InteractionUtils.CreatePolyline(data, left, top, this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15), StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), - "none", "none", "0", scaleX, scaleY, "", this.props.active() ? "visiblepainted" : "none", false, true); + "none", "none", undefined, scaleX, scaleY, "", this.props.active() ? "visiblepainted" : "none", false, true); //points for adding const apoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ec43e6e1d..f3d8fc181 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -70,7 +70,6 @@ import RichTextMenu from './nodes/formattedText/RichTextMenu'; export class MainView extends React.Component { public static Instance: MainView; private _buttonBarHeight = 36; - private _flyoutSizeOnDown = 0; private _urlState: HistoryUtil.DocUrl; private _docBtnRef = React.createRef<HTMLDivElement>(); private _mainViewRef = React.createRef<HTMLDivElement>(); @@ -423,7 +422,7 @@ export class MainView extends React.Component { onFlyoutPointerDown = (e: React.PointerEvent) => { if (this._flyoutTranslate) { setupMoveUpEvents(this, e, action((e: PointerEvent) => { - this.flyoutWidth = Math.max(e.clientX, 0); + this.flyoutWidth = Math.max(e.clientX - 58, 0); if (this.flyoutWidth < 5) { this.panelContent = "none"; this._lastButton && (this._lastButton.color = "white"); @@ -545,7 +544,12 @@ export class MainView extends React.Component { switch (this.panelContent = title) { case "Tools": panelDoc = Doc.UserDoc()["sidebar-tools"] as Doc ?? undefined; break; case "Workspace": panelDoc = Doc.UserDoc()["sidebar-workspaces"] as Doc ?? undefined; break; - case "Catalog": panelDoc = Doc.UserDoc()["sidebar-catalog"] as Doc ?? undefined; break; + case "Catalog": SearchBox.Instance.searchFullDB = "My Stuff"; + SearchBox.Instance.newsearchstring = ""; + SearchBox.Instance.enter(undefined); + break; + + // panelDoc = Doc.UserDoc()["sidebar-catalog"] as Doc ?? undefined; break; case "Archive": panelDoc = Doc.UserDoc()["sidebar-recentlyClosed"] as Doc ?? undefined; break; case "Settings": SettingsManager.Instance.open(); break; case "Import": panelDoc = Doc.UserDoc()["sidebar-import"] as Doc ?? undefined; break; @@ -978,7 +982,6 @@ export class MainView extends React.Component { } } Scripting.addGlobal(function selectMainMenu(doc: Doc, title: string) { MainView.Instance.selectMenu(doc); }); -Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); }); Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }); Scripting.addGlobal(function copyWorkspace() { const copiedWorkspace = Doc.MakeCopy(Cast(Doc.UserDoc().activeWorkspace, Doc, null), true); diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 35d4f7f6e..e220bd7fc 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -3,7 +3,7 @@ import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faChec import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../fields/Doc"; +import { Doc, DataSym, AclEdit, AclAdmin } from "../../fields/Doc"; import { RichTextField } from '../../fields/RichTextField'; import { Cast, NumCast, BoolCast } from "../../fields/Types"; import { emptyFunction, setupMoveUpEvents, Utils } from "../../Utils"; @@ -30,6 +30,7 @@ import { undoBatch, UndoManager } from '../util/UndoManager'; import { DocumentType } from '../documents/DocumentTypes'; import { InkField } from '../../fields/InkField'; import { PresBox } from './nodes/PresBox'; +import { GetEffectiveAcl } from '../../fields/util'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -76,12 +77,12 @@ export class PropertiesButtons extends React.Component<{}, {}> { public static hasPulledHack = false; + @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } @computed get selectedDocumentView() { if (SelectionManager.SelectedDocuments().length) { return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } + } else return undefined; } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } @computed get onClick() { return this.selectedDoc?.onClickBehavior ? this.selectedDoc?.onClickBehavior : "nothing"; } @@ -296,9 +297,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { } @undoBatch onAliasButtonMoved = () => { - if (this._dragRef.current) { + if (this._dragRef.current && this.selectedDoc) { const dragDocView = this.selectedDocumentView!; - const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); + const dragData = new DragManager.DocumentDragData([this.selectedDoc]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); dragData.dropAction = "alias"; DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { @@ -313,7 +314,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get templateButton() { - const docView = this.selectedDocumentView; + const docView = this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined; const templates: Map<Template, boolean> = new Map(); const views = [this.selectedDocumentView]; Array.from(Object.values(Templates.TemplateList)).map(template => @@ -365,7 +366,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { @action @undoBatch onLock = () => { - this.selectedDocumentView?.toggleLockPosition(); + const docView = this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined; + docView?.toggleLockPosition(); } @computed @@ -400,8 +402,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div> <div className={"propertiesButtons-linkButton-empty"} onPointerDown={async () => { - if (this.selectedDocumentView?.props.Document) { - Doc.Zip(this.selectedDocumentView?.props.Document); + if (this.selectedDoc) { + Doc.Zip(this.selectedDoc); } }}> {<FontAwesomeIcon className="propertiesButtons-icon" @@ -431,23 +433,22 @@ export class PropertiesButtons extends React.Component<{}, {}> { @undoBatch @action deleteDocument = () => { - const selected = SelectionManager.SelectedDocuments().slice(); - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); - this.selectedDoc && (this.selectedDoc.deleted = true); - this.selectedDocumentView?.props.ContainingCollectionView?.removeDocument(this.selectedDocumentView?.props.Document); + const removeDoc = this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView?.props.removeDocument : SelectionManager.SelectedSchemaCollection()?.props.removeDocument; + this.selectedDoc && removeDoc?.(this.selectedDoc); SelectionManager.DeselectAll(); } @computed get sharingButton() { const targetDoc = this.selectedDoc; + const docView = this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Share Document"}</div></>} placement="top"> <div> <div className={"propertiesButtons-linkButton-empty"} onPointerDown={() => { if (this.selectedDocumentView) { - SharingManager.Instance.open(this.selectedDocumentView); + SharingManager.Instance.open(docView, this.selectedDoc); } }}> {<FontAwesomeIcon className="propertiesButtons-icon" @@ -484,20 +485,21 @@ export class PropertiesButtons extends React.Component<{}, {}> { handleOptionChange = (e: any) => { const value = e.target.value; this.selectedDoc && (this.selectedDoc.onClickBehavior = e.target.value); + const docView = this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined; if (value === "nothing") { - this.selectedDocumentView?.noOnClick(); + docView?.noOnClick(); } else if (value === "enterPortal") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.makeIntoPortal(); + docView?.noOnClick(); + docView?.makeIntoPortal(); } else if (value === "toggleDetail") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleDetail(); + docView?.noOnClick(); + docView?.toggleDetail(); } else if (value === "linkInPlace") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleFollowLink("inPlace", true, false); + docView?.noOnClick(); + docView?.toggleFollowLink("inPlace", true, false); } else if (value === "linkOnRight") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleFollowLink("onRight", false, false); + docView?.noOnClick(); + docView?.toggleFollowLink("onRight", false, false); } } @@ -564,8 +566,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div> <div className={"propertiesButtons-linkButton-empty"} onPointerDown={() => { - if (this.selectedDocumentView) { - GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDocumentView.Document }).then(console.log); + if (this.selectedDoc) { + GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDoc }).then(console.log); } }}> {<FontAwesomeIcon className="documentdecorations-icon" @@ -710,6 +712,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { const isCollection = this.selectedDoc.type === DocumentType.COL ? true : false; const isFreeForm = this.selectedDoc._viewType === "freeform" ? true : false; const hasContext = this.selectedDoc.context ? true : false; + const collectionAcl = GetEffectiveAcl(this.selectedDocumentView?.props.ContainingCollectionDoc?.[DataSym]); return <div><div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}> <div className="propertiesButtons-button"> @@ -733,9 +736,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-button"> {this.downloadButton} </div> - <div className="propertiesButtons-button"> - {this.deleteButton} - </div> + {collectionAcl === AclAdmin || collectionAcl === AclEdit ? + <div className="propertiesButtons-button"> + {this.deleteButton} + </div> + : (null)} <div className="propertiesButtons-button"> {this.onClickButton} </div> diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 8658212b7..30d5103bf 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -229,14 +229,18 @@ .flexLabel { margin-bottom: 0; } - } - .collectionGridViewChrome-entryBox { - width: 50%; + .collectionGridViewChrome-entryBox { + width: 50%; + color: black; + } + + .collectionGridViewChrome-columnButton { + color: black; + } } } - .collectionStackingViewChrome-sort, .collectionTreeViewChrome-sort { display: flex; diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 97d38d27e..b2e55adc7 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -686,7 +686,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || - key.indexOf("lastModified") >= 0 || (key[0].toUpperCase() === key[0] && + key.indexOf("lastModified") >= 0 || (key[0]?.toUpperCase() === key[0] && key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } @@ -935,16 +935,10 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp get numCols() { return NumCast(this.document.gridNumCols, 10); } /** - * Sets the value of `numCols` on the grid's Document to the value entered. - */ - @undoBatch - onNumColsEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter" || e.key === "Tab") { - if (e.currentTarget.valueAsNumber > 0) { - this.document.gridNumCols = e.currentTarget.valueAsNumber; - } - - } + * Sets the value of `numCols` on the grid's Document to the value entered. + */ + onNumColsChange = (e: React.ChangeEvent<HTMLInputElement>) => { + if (e.currentTarget.valueAsNumber > 0) undoBatch(() => this.document.gridNumCols = e.currentTarget.valueAsNumber)(); } /** @@ -982,9 +976,10 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp */ onDecrementButtonClick = () => { this.clicked = true; - if (!this.decrementLimitReached) { + if (this.numCols > 1 && !this.decrementLimitReached) { this.entered && (this.document.gridNumCols as number)++; undoBatch(() => this.document.gridNumCols = this.numCols - 1)(); + if (this.numCols === 1) this.decrementLimitReached = true; } this.entered = false; } @@ -1007,7 +1002,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp decrementValue = () => { this.entered = true; if (!this.clicked) { - if (this.numCols !== 1) { + if (this.numCols > 1) { this.document.gridNumCols = this.numCols - 1; } else { @@ -1040,9 +1035,9 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp <span className="grid-icon"> <FontAwesomeIcon icon="columns" size="1x" /> </span> - <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.numCols.toString()} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> - <input className="columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" /> - <input className="columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" /> + <input className="collectionGridViewChrome-entryBox" type="number" value={this.numCols} onChange={this.onNumColsChange} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> + <input className="collectionGridViewChrome-columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" /> + <input className="collectionGridViewChrome-columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" /> </span> {/* <span className="grid-control"> <span className="grid-icon"> diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 003a1e9ff..f95de5201 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -71,15 +71,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { async componentDidMount() { document.addEventListener("keydown", this.onKeyDown); - if (this.type === "context") { - const doc = Doc.GetProto(this.props.rowProps.original); - const aliasdoc = await SearchUtil.GetAliasesOfDocument(doc); - if (aliasdoc.length > 0) { - const targetContext = Cast(aliasdoc[0].context, Doc, null); - targetContext && runInAction(() => this.contents = StrCast(targetContext.title)); - } - } - } @observable contents: string = ""; @@ -266,7 +257,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { }; let contents: any = "incorrect type"; - if (type === undefined) contents = StrCast(field) === "" ? "--" : <FieldView {...props} fieldKey={fieldKey} />; + if (type === undefined) contents = field === undefined ? undefined : Field.toString(field as Field);//StrCast(field) === "" ? "--" : <FieldView {...props} fieldKey={fieldKey} />; if (type === "number") contents = typeof field === "number" ? NumCast(field) : "--" + typeof field + "--"; if (type === "string") { fieldKey === "text" ? @@ -303,10 +294,10 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // </div> // ); const positions = []; - let cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); + let cfield = props.Document[props.fieldKey]; this.type = props.fieldKey; if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { - let term = Field.toString(cfield as Field); + let term = (cfield instanceof Promise) ? "...promise pending..." : Field.toString(cfield as Field); term = term.toLowerCase(); const search = StrCast(this.props.Document._searchString).toLowerCase(); let start = term.indexOf(search); @@ -329,6 +320,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { search = true; } + const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; return ( <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> @@ -338,16 +330,16 @@ export class CollectionSchemaCell extends React.Component<CellProps> { {!search ? <EditableView positions={positions.length > 0 ? positions : undefined} - search={StrCast(this.props.Document._searchString) ? StrCast(this.props.Document._searchString) : undefined} + search={Cast(this.props.Document._searchString, "string", null)} editing={this._isEditing} isEditingCallback={this.isEditingCallback} display={"inline"} - contents={contents ? contents : type === "number" ? "0" : "undefined"} + contents={contents} highlight={positions.length > 0 ? true : undefined} //contents={StrCast(contents)} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} - placeholder={"undefined"} + placeholder={placeholder} bing={() => { const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); if (cfield !== undefined) { @@ -389,8 +381,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> { SetValue={action((value: string) => { let retVal = false; - if (value.startsWith(":=")) { - retVal = this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); + if (value.startsWith(":=") || value.startsWith("=:=")) { + const script = value.substring(value.startsWith("=:=") ? 3 : 2); + retVal = this.props.setComputed(script, value.startsWith(":=") ? Doc.GetProto(props.Document) : props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); } else { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (script.compiled) { diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index b9390b1a0..ac8525d0b 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -520,7 +520,6 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } @computed get scriptField() { - console.log("we kinda made it"); const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); return script ? () => script : undefined; @@ -553,7 +552,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { render() { return ( <div style={{ display: "flex" }}> - <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> + <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> {/* <FontAwesomeIcon icon={fa.faSearchMinus} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} onClick={e => { runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen }) diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index dade4f2f2..37e6c115d 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -40,7 +40,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> { onPointerLeave = (e: React.PointerEvent): void => { this._header!.current!.className = "collectionSchema-col-wrapper"; document.removeEventListener("pointermove", this.onDragMove, true); - document.removeEventListener("pointermove", this.onPointerMove); + !e.buttons && document.removeEventListener("pointermove", this.onPointerMove); } onDragMove = (e: PointerEvent): void => { const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); @@ -68,6 +68,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> { const before = x[0] < bounds[0]; const colDragData = de.complete.columnDragData; if (colDragData) { + e.stopPropagation(); this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns); return true; } @@ -108,8 +109,10 @@ export class MovableColumn extends React.Component<MovableColumnProps> { onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => { this._dragRef = ref; const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY); - this._startDragPosition = { x: dx, y: dy }; - document.addEventListener("pointermove", this.onPointerMove); + if (!(e.target as any)?.tagName.includes("INPUT")) { + this._startDragPosition = { x: dx, y: dy }; + document.addEventListener("pointermove", this.onPointerMove); + } } @@ -164,6 +167,10 @@ export class MovableRow extends React.Component<MovableRowProps> { if (!before) this._header!.current!.className += " row-below"; e.stopPropagation(); } + componentWillUnmount() { + + this._rowDropDisposer?.(); + } createRowDropTarget = (ele: HTMLDivElement) => { this._rowDropDisposer?.(); diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 3683fdffd..c1918aed0 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -250,6 +250,7 @@ button.add-column { .collectionSchema-headerMenu-group { padding: 7px 0; border-bottom: 1px solid lightgray; + cursor: pointer; &:first-child { padding-top: 0; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index b0601afca..ed8496544 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; import Measure from "react-measure"; import { Resize } from "react-table"; import "react-table/react-table.css"; -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt } from "../../../fields/Doc"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; @@ -20,10 +20,12 @@ import { undoBatch } from "../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import '../DocumentDecorations.scss'; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { KeysDropdown } from "./CollectionSchemaHeaders"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { SchemaTable } from "./SchemaTable"; +import { SelectionManager } from "../../util/SelectionManager"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); @@ -163,11 +165,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.columns = columns; } - @action - typesDropdownChange = (bool: boolean) => { - this._openTypes = bool; - } - renderTypes = (col: any) => { if (columnTypes.get(col.heading)) return (null); @@ -231,10 +228,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { type === ColumnType.Date ? dateType : imageType; return ( - <div className="collectionSchema-headerMenu-group"> - <div onClick={() => this.typesDropdownChange(!this._openTypes)}> - <label>Column type:</label> - <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right" }} /> + <div className="collectionSchema-headerMenu-group" onClick={action(() => this._openTypes = !this._openTypes)}> + <div> + <label style={{ cursor: "pointer" }}>Column type:</label> + <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right", transform: `rotate(${this._openTypes ? "180deg" : 0})`, transition: "0.2s all ease" }} /> </div> {this._openTypes ? allColumnTypes : justColType} </div > @@ -328,7 +325,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action openHeader = (col: any, screenx: number, screeny: number) => { this._col = col; - this._headerOpen = !this._headerOpen; + this._headerOpen = true; this._pointerX = screenx; this._pointerY = screeny; } @@ -360,7 +357,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action onHeaderClick = (e: React.PointerEvent) => { - this.props.active(true); e.stopPropagation(); } @@ -394,7 +390,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setFocused = (doc: Doc) => this._focusedTable = doc; - @action setPreviewDoc = (doc: Doc) => this.previewDoc = doc; + @action setPreviewDoc = (doc: Opt<Doc>) => { + SelectionManager.SelectSchemaDoc(this, doc); + this.previewDoc = doc; + } //toggles preview side-panel of schema @action @@ -499,6 +498,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { documentKeys={this.documentKeys} headerIsEditing={this._headerIsEditing} openHeader={this.openHeader} + onClick={e => { e.stopPropagation(); this.closeHeader(); }} onPointerDown={this.onTablePointerDown} onResizedChange={this.onResizedChange} setColumns={this.setColumns} @@ -520,15 +520,29 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div> </div>; } + onSpecificMenu = (e: React.MouseEvent) => { + if ((e.target as any)?.className?.includes?.("collectionSchemaView-cell") || (e.target instanceof HTMLSpanElement)) { + const cm = ContextMenu.Instance; + const options = cm.findByDescription("Options..."); + const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; + optionItems.push({ description: "remove", event: () => this.previewDoc && this.props.removeDocument(this.previewDoc), icon: "trash" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); + cm.displayMenu(e.clientX, e.clientY); + (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. + e.stopPropagation(); + } + } @action onTablePointerDown = (e: React.PointerEvent): void => { + if (!(e.target as any)?.className?.includes?.("collectionSchemaView-cell") && !(e.target instanceof HTMLSpanElement)) { + this.setPreviewDoc(undefined); + } this.setFocused(this.props.Document); if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) { e.stopPropagation(); } - this._pointerY = e.screenY; - this._pointerX = e.screenX; + // this.closeHeader(); } onResizedChange = (newResized: Resize[], event: any) => { @@ -581,6 +595,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { if (this.props.Document._searchDoc !== undefined) { name = "collectionSchemaView-searchContainer"; } + if (!this.props.active()) setTimeout(() => this.closeHeader(), 0); TraceMobx(); const menuContent = this.renderMenuContent; const menu = <div className="collectionSchema-header-menu" @@ -588,7 +603,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { onPointerDown={e => this.onHeaderClick(e)} style={{ position: "fixed", background: "white", border: "black 1px solid", - transform: `translate(${(this.menuCoordinates[0] / this.scale)}px, ${(this.menuCoordinates[1] / this.scale)}px)` + transform: `translate(${(this.menuCoordinates[0])}px, ${(this.menuCoordinates[1])}px)` }}> <Measure offset onResize={action((r: any) => { const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); @@ -606,6 +621,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchemaView-tableContainer" style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onKeyPress={this.onKeyPress} + onContextMenu={this.onSpecificMenu} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} @@ -614,7 +630,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div> {this.dividerDragger} {!this.previewWidth() ? (null) : this.previewPanel} - {this._headerOpen ? menu : null} + {this._headerOpen && this.props.active() ? menu : null} </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index fe3d57bdb..241c64f9a 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -47,7 +47,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } - @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, NumCast(this.layoutDoc._yMargin, 0)); } // 2 * this.gridGap)); } + @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, NumCast(this.layoutDoc._yMargin, 5)); } // 2 * this.gridGap)); } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @computed get isStackingView() { return BoolCast(this.layoutDoc._columnsStack, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index f193a9787..ede75fba8 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -289,7 +289,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const style = this.props.parent; const singleColumn = style.isStackingView; - const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin); + const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin, 5); const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const headerEditableViewProps = { @@ -310,7 +310,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ - marginTop: NumCast(this.props.parent.props.Document._yMargin), + marginTop: NumCast(this.props.parent.props.Document._yMargin, 5), width: (style.columnWidth) / ((uniqueHeadings.length + ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1) diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 9d305145e..0aaceb7f4 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -208,7 +208,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus toRemove.forEach(doc => { Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc); recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); - doc.deleted = true; }); return true; } @@ -290,7 +289,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus subItems.push({ description: "Tree", event: () => func(CollectionViewType.Tree), icon: "tree" }); subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking), icon: "ellipsis-v" }); subItems.push({ description: "Stacking (AutoHeight)", event: () => func(CollectionViewType.Stacking)._autoHeight = true, icon: "ellipsis-v" }); - subItems.push({ description: "Staff", event: () => func(CollectionViewType.Staff), icon: "music" }); subItems.push({ description: "Multicolumn", event: () => func(CollectionViewType.Multicolumn), icon: "columns" }); subItems.push({ description: "Multirow", event: () => func(CollectionViewType.Multirow), icon: "columns" }); subItems.push({ description: "Masonry", event: () => func(CollectionViewType.Masonry), icon: "columns" }); diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index 7324dd6df..904eaf99f 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -42,7 +42,7 @@ enum ColumnType { // this map should be used for keys that should have a const type of value const columnTypes: Map<string, ColumnType> = new Map([ - ["title", ColumnType.String], ["text", ColumnType.String], + ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] @@ -70,11 +70,12 @@ export interface SchemaTableProps { isSelected: (outsideReaction?: boolean) => boolean; isFocused: (document: Doc, outsideReaction: boolean) => boolean; setFocused: (document: Doc) => void; - setPreviewDoc: (document: Doc) => void; + setPreviewDoc: (document: Opt<Doc>) => void; columns: SchemaHeaderField[]; documentKeys: any[]; headerIsEditing: boolean; openHeader: (column: any, screenx: number, screeny: number) => void; + onClick: (e: React.MouseEvent) => void; onPointerDown: (e: React.PointerEvent) => void; onResizedChange: (newResized: Resize[], event: any) => void; setColumns: (columns: SchemaHeaderField[]) => void; @@ -230,7 +231,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return { Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.props.columns} reorderColumns={this.props.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />, - accessor: (doc: Doc) => doc ? doc[col.heading] : 0, + accessor: (doc: Doc) => doc ? Field.toString(doc[col.heading] as Field) : 0, id: col.heading, Cell: (rowProps: CellInfo) => { const rowIndex = rowProps.index; @@ -321,8 +322,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const newSchemaHeaders = oldSchemaHeaders.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); this.props.Document._schemaHeaders = new List<SchemaHeaderField>(newSchemaHeaders); } else if (this.props.Document._schemaHeaders === undefined) { - this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb"), new SchemaHeaderField("author", "#f1efeb"), new SchemaHeaderField("*lastModified", "#f1efeb"), - new SchemaHeaderField("text", "#f1efeb"), new SchemaHeaderField("type", "#f1efeb"), new SchemaHeaderField("context", "#f1efeb")]); + this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb"), new SchemaHeaderField("author", "#f1efeb"), new SchemaHeaderField("*lastModified", "#f1efeb", ColumnType.Date), + new SchemaHeaderField("text", "#f1efeb", ColumnType.String), new SchemaHeaderField("type", "#f1efeb"), new SchemaHeaderField("context", "#f1efeb", ColumnType.Doc)]); } } @@ -384,7 +385,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); pdoc && this.props.setPreviewDoc(pdoc); - } else if ((this._cellIsEditing || this.props.headerIsEditing) && (e.keyCode === 37 || e.keyCode === 39)) { + e.stopPropagation(); + } else if (e.keyCode === 27) { + this.props.setPreviewDoc(undefined); e.stopPropagation(); // stopPropagation for left/right arrows } } @@ -409,9 +412,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @undoBatch - createRow = () => { + createRow = action(() => { this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); - } + this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col }; + }) @undoBatch @action @@ -563,10 +567,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => { script = `const $ = (row:number, col?:number) => { - if(col === undefined) { - return (doc as any)[key][row + ${row}]; - } - return (doc as any)[key][row + ${row}][(doc as any)._schemaHeaders[col + ${col}].heading]; + const rval = (doc as any)[key][row + ${row}]; + return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading]; } return ${script}`; const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) }); @@ -598,7 +600,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { render() { const preview = ""; return <div className="collectionSchemaView-table" style={{ overflow: this.props.Document._searchDoc ? undefined : "auto" }} - onPointerDown={this.props.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} + onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} {StrCast(this.props.Document.type) !== "search" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 46e30f616..5d6d7924e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -850,7 +850,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } else { const docs = this.childLayoutPairs.map(pair => pair.layout); docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - let zlast = docs.length ? NumCast(docs[docs.length - 1].zIndex) : 1; + let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1; if (zlast - docs.length > 100) { for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; zlast = docs.length + 1; @@ -1315,7 +1315,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @action - setupDragLines = () => { + setupDragLines = (snapToDraggedDoc: boolean = false) => { const activeDocs = this.getActiveDocuments(); if (activeDocs.length > 50) { DragManager.SetSnapLines([], []); @@ -1337,7 +1337,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const horizLines: number[] = []; const vertLines: number[] = []; - snappableDocs.filter(doc => !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { + snappableDocs.filter(doc => snapToDraggedDoc || !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = this.getTransform().inverse().transformPoint(left, top); const docSize = this.getTransform().inverse().transformDirection(width, height); @@ -1349,7 +1349,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } onPointerOver = (e: React.PointerEvent) => { if (SnappingManager.GetIsDragging()) { - this.setupDragLines(); + this.setupDragLines(e.ctrlKey || e.shiftKey); } e.stopPropagation(); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c0b19fcd2..b6a0504e2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -12,7 +12,7 @@ import { CognitiveServices } from "../../../cognitive_services/CognitiveServices import { Docs, DocumentOptions, DocUtils } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { ContextMenu } from "../../ContextMenu"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { PreviewCursor } from "../../PreviewCursor"; @@ -138,6 +138,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque tbox.layoutKey = "layout_" + StrCast(template.title); Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; } + FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); this.props.addLiveTextDocument(tbox); e.stopPropagation(); } diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss index aee28366a..535581f2e 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.scss +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.scss @@ -121,6 +121,19 @@ padding: 10px; margin-left: 5px; + .propertiesView-acls-checkbox { + float: right; + height: 20px; + margin-top: -20px; + margin-right: -15; + + .propertiesView-acls-checkbox-text { + font-size: 7px; + margin-top: -10px; + margin-left: 6px; + } + } + .change-buttons { display: flex; @@ -259,6 +272,7 @@ background-color: #ececec; max-height: 130px; overflow-y: scroll; + width: 92%; .propertiesView-sharingTable-item { @@ -267,7 +281,6 @@ padding: 3px; align-items: center; border-bottom: 0.5px solid grey; - cursor: pointer; &:hover .propertiesView-sharingTable-item-name { overflow-x: unset; diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx index 57e968aa7..fb138ecc0 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { observer } from "mobx-react"; import "./PropertiesView.scss"; import { observable, action, computed, runInAction } from "mobx"; -import { Doc, Field, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt, DocCastAsync } from "../../../../fields/Doc"; +import { Doc, Field, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt, DocCastAsync, DataSym } from "../../../../fields/Doc"; import { ComputedField } from "../../../../fields/ScriptField"; import { EditableView } from "../../EditableView"; import { KeyValueBox } from "../../nodes/KeyValueBox"; @@ -49,6 +49,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get MAX_EMBED_HEIGHT() { return 200; } + @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } @computed get selectedDocumentView() { if (SelectionManager.SelectedDocuments().length) { return SelectionManager.SelectedDocuments()[0]; @@ -60,7 +61,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (this.selectedDoc?.type === DocumentType.PRES) return true; return false; } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } @observable layoutFields: boolean = false; @@ -73,6 +73,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openTransform: boolean = true; // @observable selectedUser: string = ""; // @observable addButtonPressed: boolean = false; + @observable layoutDocAcls: boolean = false; //Pres Trails booleans: @observable openPresTransitions: boolean = false; @@ -344,8 +345,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get expansionIcon() { return <Tooltip title={<div className="dash-tooltip">{"Show more permissions"}</div>}> <div className="expansion-button" onPointerDown={() => { - if (this.selectedDocumentView) { - SharingManager.Instance.open(this.selectedDocumentView); + if (this.selectedDocumentView || this.selectedDoc) { + SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDocumentView ? this.selectedDocumentView : undefined, this.selectedDoc); } }}> <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" /> @@ -382,15 +383,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { [AclAdmin, SharingPermissions.Admin] ]); - const effectiveAcl = GetEffectiveAcl(this.selectedDoc!); + const target = this.layoutDocAcls ? this.selectedDoc! : this.selectedDoc![DataSym]; + + const effectiveAcl = GetEffectiveAcl(target); const tableEntries = []; // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { - if (this.selectedDoc![AclSym]) { - for (const [key, value] of Object.entries(this.selectedDoc![AclSym])) { + if (target[AclSym]) { + for (const [key, value] of Object.entries(target[AclSym])) { const name = key.substring(4).replace("_", "."); - if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author/* && sidebarUsersDisplayed![name] !== false*/) { - tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!)); + if (name !== Doc.CurrentUserEmail && name !== target.author && name !== "Public"/* && sidebarUsersDisplayed![name] !== false*/) { + tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value as symbol)!)); } } } @@ -402,9 +405,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { // } // }) - // shifts the current user and the owner to the top of the doc. - tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === this.selectedDoc!.author ? "Owner" : StrCast(this.selectedDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]))); - if (Doc.CurrentUserEmail !== this.selectedDoc!.author) tableEntries.unshift(this.sharingItem(StrCast(this.selectedDoc!.author), effectiveAcl, "Owner")); + // shifts the current user, owner, public to the top of the doc. + tableEntries.unshift(this.sharingItem("Public", effectiveAcl, (AclMap.get(target[AclSym]?.["ACL-Public"]) || SharingPermissions.None))); + tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === target.author ? "Owner" : AclMap.get(effectiveAcl)!)); + if (Doc.CurrentUserEmail !== target.author) tableEntries.unshift(this.sharingItem(StrCast(target.author), effectiveAcl, "Owner")); return <div className="propertiesView-sharingTable"> {tableEntries} @@ -866,6 +870,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> {!this.openSharing ? (null) : <div className="propertiesView-sharing-content"> + <div className="propertiesView-acls-checkbox"> + <Checkbox + color="primary" + onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} + checked={this.layoutDocAcls} + />; + <div className="propertiesView-acls-checkbox-text">Layout</div> + </div> {this.sharingTable} {/* <div className="change-buttons"> <button diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index e6ac7021a..70ebca5e7 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -94,8 +94,8 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { */ unflexedPosition(index: number): Omit<Layout, "i"> { return { - x: (index % Math.floor(this.numCols / this.defaultW)) * this.defaultW, - y: Math.floor(index / Math.floor(this.numCols / this.defaultH)) * this.defaultH, + x: (index % (Math.floor(this.numCols / this.defaultW) || 1)) * this.defaultW, + y: Math.floor(index / (Math.floor(this.numCols / this.defaultH) || 1)) * this.defaultH, w: this.defaultW, h: this.defaultH, static: true diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ede0d8c99..9ae113150 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -69,7 +69,7 @@ export interface DocumentViewProps { removeDocument?: (doc: Doc | Doc[]) => boolean; moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - setupDragLines?: () => void; + setupDragLines?: (snapToDraggedDoc: boolean) => void; renderDepth: number; ContentScaling: () => number; PanelWidth: () => number; @@ -291,7 +291,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let stopPropagate = true; let preventDefault = true; !this.props.Document.isBackground && this.props.bringToFront(this.props.Document); - if (this._doubleTap && this.props.renderDepth) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._doubleTap && this.props.renderDepth && (this.props.Document.type !== DocumentType.FONTICON || !this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click if (!(e.nativeEvent as any).formattedHandled) { if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself const func = () => this.onDoubleClickHandler.script.run({ @@ -684,7 +684,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action - onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => { + onContextMenu = (e: React.MouseEvent | Touch) => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (!(e instanceof Touch)) { if (e.button === 0 && !e.ctrlKey) { @@ -703,7 +703,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } const cm = ContextMenu.Instance; - if (!cm) return; + if (!cm || ((e as any)?.nativeEvent as any)?.SchemaHandled) return; const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => @@ -763,7 +763,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()) && moreItems.push({ description: "Toggle Always Show Link End", event: () => Doc.UserDoc()["documentLinksButton-hideEnd"] = !Doc.UserDoc()["documentLinksButton-hideEnd"], icon: "eye" }); } - moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); + const collectionAcl = GetEffectiveAcl(this.props.ContainingCollectionDoc?.[DataSym]); + if (collectionAcl === AclAdmin || collectionAcl === AclEdit) moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 9ac3ea847..0f2f90c1d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -509,7 +509,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (node.isTextblock) { let index = 0, foundAt; const ep = this.getNodeEndpoints(pm.state.doc, node); - while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) { + const regexp = find.replace("*", ""); + if (regexp) while (ep && (foundAt = node.textContent.slice(index).search(regexp, "i")) > -1) { const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); ret.push(sel); index = index + foundAt + find.length; @@ -1433,6 +1434,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } return wasUndoing; } + public static LiveTextUndo: UndoManager.Batch | undefined; public static HadSelection: boolean = false; onBlur = (e: any) => { FormattedTextBox.HadSelection = window.getSelection()?.toString() !== ""; @@ -1440,6 +1442,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.endUndoTypingBatch(); this.doLinkOnDeselect(); + FormattedTextBox.LiveTextUndo?.end(); + FormattedTextBox.LiveTextUndo = undefined; // move the richtextmenu offscreen //if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); } @@ -1526,7 +1530,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const scale = this.props.hideOnLeave ? 1 : this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; - setTimeout(() => this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props), this.props.isSelected() ? 10 : 0); // need to make sure that we update a text box that is selected after updating the one that was deselected + this.props.isSelected() && setTimeout(() => this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props), 0); // need to make sure that we update a text box that is selected after updating the one that was deselected if (!this.props.isSelected() && FormattedTextBoxComment.textBox === this) { setTimeout(() => FormattedTextBoxComment.Hide(), 0); } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index e5cc62082..213b341e8 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -253,7 +253,9 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const pos = this.view.state.selection.$from; const ref_node = this.reference_node(pos); if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) { - ref_node.marks.forEach(m => { + const marks = Array.from(ref_node.marks); + marks.push(...(this.view.state.storedMarks as any)); + marks.forEach(m => { m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt"); @@ -428,14 +430,20 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if ((this.view?.state.selection.$from.pos || 0) < 2) { this.TextView.layoutDoc._fontSize = mark.attrs.fontSize; } - this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch, true); + const fmark = view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }); + this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); + view.focus(); + this.updateMenu(view, undefined, this.props); } changeFontFamily = (mark: Mark, view: EditorView) => { if ((this.view?.state.selection.$from.pos || 0) < 2) { this.TextView.layoutDoc._fontFamily = mark.attrs.family; } - this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch, true); + const fmark = view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }); + this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); + view.focus(); + this.updateMenu(view, undefined, this.props); } // TODO: remove doesn't work @@ -471,6 +479,8 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.view.dispatch(tx3); } } + this.view.focus(); + this.updateMenu(this.view, undefined, this.props); } insertSummarizer(state: EditorState<any>, dispatch: any) { @@ -675,16 +685,22 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { e.preventDefault(); e.stopPropagation(); self.TextView.endUndoTypingBatch(); - UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); - self.TextView.EditorView!.focus(); + if (self.view) { + UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); + self.view.focus(); + self.updateMenu(self.view, undefined, self.props); + } } function changeColor(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); self.setActiveColor(color); self.TextView.endUndoTypingBatch(); - UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); - self.TextView.EditorView!.focus(); + if (self.view) { + UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); + self.view.focus(); + self.updateMenu(self.view, undefined, self.props); + } } // onPointerDown={onColorClick} @@ -973,7 +989,7 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => this.activeFontSize = val)), this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => this.activeFontFamily = val)), <div className="richTextMenu-divider" key="divider 4" />, - this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", action((val: string) => this.activeListType = val)), + this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule), diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index c9c6aa631..10a45b34a 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -4,7 +4,7 @@ import { action, computed, observable, runInAction, reaction, IReactionDisposer import { observer } from 'mobx-react'; import * as React from 'react'; import * as rp from 'request-promise'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -24,6 +24,7 @@ import { ViewBoxBaseComponent } from "../DocComponent"; import { DocumentView } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./SearchBox.scss"; +import { ColumnType } from "../collections/CollectionSchemaView"; export const searchSchema = createSchema({ id: "string", @@ -188,21 +189,14 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc } } - enter = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { + enter = action((e: React.KeyboardEvent | undefined) => { + if (!e || e.key === "Enter") { this.layoutDoc._searchString = this.newsearchstring; - runInAction(() => this._pageStart = 0); - - if (StrCast(this.layoutDoc._searchString) !== "" || !this.searchFullDB) { - runInAction(() => this.open = true); - } - else { - runInAction(() => this.open = false); - - } + this._pageStart = 0; + this.open = StrCast(this.layoutDoc._searchString) !== "" || this.searchFullDB !== "DB"; this.submitSearch(); } - } + }); @observable open: boolean = false; @@ -239,6 +233,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc @observable private _authorFieldStatus: boolean = true; //this also serves as an indicator if the collection status filter is applied @observable public _deletedDocsStatus: boolean = false; + @observable public _onlyAliases: boolean = true; @observable private _collectionStatus = false; @@ -575,7 +570,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc }, 60000); } - if (query !== "") { + if (query !== "" || this.searchFullDB === "My Stuff") { this._endIndex = 12; this._maxSearchIndex = 0; this._numTotalResults = -1; @@ -590,7 +585,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc } } - @observable searchFullDB = true; + @observable searchFullDB = "DB"; @observable _timeout: any = undefined; @@ -607,15 +602,28 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc private get filterQuery() { const types = ["preselement", "docholder", "search", "searchitem", "fonticonbox"]; // this.filterTypes; const baseExpr = "NOT system_b:true"; + const authorExpr = this.searchFullDB === "My Stuff" ? ` author_t:${Doc.CurrentUserEmail}` : undefined; const includeDeleted = this.getDataStatus() ? "" : " NOT deleted_b:true"; - const typeExpr = !types ? "" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${types.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; + const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${types.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello - const query = [baseExpr, includeDeleted, typeExpr].join(" AND ").replace(/AND $/, ""); + const query = [baseExpr, authorExpr, includeDeleted, typeExpr].filter(q => q).join(" AND ").replace(/AND $/, ""); return query; } getDataStatus() { return this._deletedDocsStatus; } + @computed get primarySort() { + const suffixMap = (type: ColumnType) => { + switch (type) { + case ColumnType.Date: return "_d"; + case ColumnType.String: return "_t"; + case ColumnType.Boolean: return "_b"; + case ColumnType.Number: return "_n"; + } + }; + const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); + return headers.reduce((p: Opt<string>, header: SchemaHeaderField) => p || (header.desc !== undefined && suffixMap(header.type) ? (header.heading + suffixMap(header.type) + (header.desc ? " desc" : " asc")) : undefined), undefined); + } private NumResults = 50; private lockPromise?: Promise<void>; getResults = async (query: string) => { @@ -624,7 +632,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc } this.lockPromise = new Promise(async res => { while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { - this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: this.NumResults, hl: true, "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { + this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, sort: this.primarySort, fq: this.filterQuery, start: 0, rows: this.NumResults, hl: true, "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { // happens at the beginning this.realTotalResults = res.numFound <= 0 ? 0 : res.numFound; if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { @@ -634,7 +642,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const highlightList = res.docs.map(doc => highlighting[doc[Id]]); const lines = new Map<string, string[]>(); res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i])); - const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); + const docs = res.docs; const highlights: typeof res.highlighting = {}; docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); const filteredDocs = this.filterDocsByType(docs); @@ -882,10 +890,10 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc <div style={{ pointerEvents: "all" }} className="searchBox-container"> <div style={{ position: "absolute", left: 15, height: 32, alignItems: "center", display: "flex" }}>{Doc.CurrentUserEmail}</div> <div className="searchBox-bar"> - <div style={{ position: "relative", display: "flex", width: 400 }}> + <div style={{ position: "relative", display: "flex", width: 450 }}> <input value={this.newsearchstring} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} - style={{ padding: 1, paddingLeft: 20, paddingRight: 20, color: "black", height: 20, width: 250 }} /> + style={{ padding: 1, paddingLeft: 20, paddingRight: 60, color: "black", height: 20, width: 250 }} /> <div style={{ display: "flex", alignItems: "center" }}> <div style={{ position: "absolute", left: 10 }}> <Tooltip title={<div className="dash-tooltip" >drag search results as collection</div>}> @@ -967,7 +975,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc <label style={{ fontSize: 12, marginTop: 6 }} > <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this.searchFullDB} onChange={() => { runInAction(() => { - this.searchFullDB = !this.searchFullDB; + this.searchFullDB = ""; this.dataDoc[this.fieldKey] = new List<Doc>([]); if (this.currentSelectedCollection !== undefined) { let newarray: Doc[] = []; @@ -999,9 +1007,9 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc </div> <div className="radio" style={{ margin: 0 }}> <label style={{ fontSize: 12, marginTop: 6 }} > - <input style={{ marginLeft: -16, marginTop: -1 }} type="radio" checked={this.searchFullDB} onChange={() => { + <input style={{ marginLeft: -16, marginTop: -1 }} type="radio" checked={this.searchFullDB?.length ? true : false} onChange={() => { runInAction(() => { - this.searchFullDB = !this.searchFullDB; + this.searchFullDB = "DB"; this.dataDoc[this.fieldKey] = new List<Doc>([]); this.filter = false; if (this.currentSelectedCollection !== undefined) { @@ -1030,7 +1038,10 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc }); }} /> DB - </label> + <span onClick={action(() => this.searchFullDB = this.searchFullDB === "My Stuff" ? "DB" : "My Stuff")}> + {this.searchFullDB === "My Stuff" ? "(me)" : "(full)"} + </span> + </label> </div> </div> </form> diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 5f5fe98bc..0dcb8ab42 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -195,6 +195,8 @@ export class Doc extends RefField { @observable private ___fieldKeys: any = {}; + @observable + public [AclSym]: { [key: string]: symbol }; private [UpdatingFromServer]: boolean = false; @@ -204,17 +206,9 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; - public [FieldsSym] = (clear?: boolean) => { - if (clear) { - this.___fields = {}; - this.___fieldKeys = {}; - } - return this.___fields; - } - @observable - public [AclSym]: { [key: string]: symbol }; - public [WidthSym] = () => NumCast(this[SelfProxy]._width); - public [HeightSym] = () => NumCast(this[SelfProxy]._height); + public [FieldsSym](clear?: boolean) { return clear ? this.___fields = this.___fieldKeys = {} : this.___fields; } + public [WidthSym]() { return NumCast(this[SelfProxy]._width); } + public [HeightSym]() { return NumCast(this[SelfProxy]._height); } public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; } public [ToString]() { return `Doc(${GetEffectiveAcl(this) === AclPrivate ? "-inaccessible-" : this.title})`; } public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; } diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index 2ca5ac082..ae5f301d0 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -28,7 +28,7 @@ export class RichTextField extends ObjectField { } [ToScriptString]() { - return `new RichTextField("${this.Data}", "${this.Text}")`; + return `new RichTextField("${this.Data.replace(/"/g, "\\\"")}", "${this.Text}")`; } [ToString]() { return this.Text; diff --git a/src/fields/util.ts b/src/fields/util.ts index 3d832636f..d8523dfa2 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -5,7 +5,7 @@ import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; import { action, trace } from "mobx"; -import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate } from "./FieldSymbols"; +import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; @@ -154,14 +154,16 @@ export enum SharingPermissions { /** * Calculates the effective access right to a document for the current user. */ -export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol { +export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { if (!target) return AclPrivate; if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { + const userChecked = user || Doc.CurrentUserEmail; + // if the current user is the author of the document / the current user is a member of the admin group - if (Doc.CurrentUserEmail === (target.__fields?.author || target.author) || currentUserGroups.includes("admin")) return AclAdmin; + if (userChecked === (target.__fields?.author || target.author) || currentUserGroups.includes("admin")) return AclAdmin; // if the ACL is being overriden or the property being modified is one of the playground fields (which can be freely modified) if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; @@ -178,7 +180,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number) for (const [key, value] of Object.entries(target[AclSym])) { // 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. - if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) { + if (currentUserGroups.includes(key.substring(4).replace("_", ".")) || userChecked === key.substring(4).replace("_", ".")) { if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; if (effectiveAcl === AclAdmin) break; @@ -208,52 +210,53 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc ["Admin", 4] ]); - let changed = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) + let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) + let dataDocChanged = false; const dataDoc = target[DataSym]; // 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 (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) { target[key] = acl; - changed = true; + layoutDocChanged = true; + } + + if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { + dataDoc[key] = acl; + dataDocChanged = true; // maps over the aliases of the document - const aliases = DocListCast(target.aliases); + const aliases = DocListCast(dataDoc.aliases); if (aliases.length) { aliases.map(alias => { alias !== target && distributeAcls(key, acl, alias, inheritingFromCollection); }); } - } - - if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { - dataDoc[key] = acl; - changed = true; - // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { - if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { distributeAcls(key, acl, d, inheritingFromCollection); } const data = d[DataSym]; - if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + if (data && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { distributeAcls(key, acl, data, inheritingFromCollection); } }); // maps over the annotations of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { - if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { distributeAcls(key, acl, d, inheritingFromCollection); } const data = d[DataSym]; - if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + if (data && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { distributeAcls(key, acl, data, inheritingFromCollection); } }); } - changed && fetchProto(target); // updates target[AclSym] when changes to acls have been made + layoutDocChanged && fetchProto(target); // updates target[AclSym] when changes to acls have been made + dataDocChanged && fetchProto(dataDoc); } const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox", @@ -286,7 +289,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; - if (in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; + if (in_prop === ToString || in_prop === ToScriptString || in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; if (prop === LayoutSym) { |