diff options
25 files changed, 1014 insertions, 281 deletions
diff --git a/.gitignore b/.gitignore index 6d4b98289..aeb343fd1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ src/server/session_manager/logs/**/*.log *.crt *.key *.pfx -*.properties
\ No newline at end of file +*.properties +debug.log
\ No newline at end of file diff --git a/debug.log b/debug.log new file mode 100644 index 000000000..ff92d4544 --- /dev/null +++ b/debug.log @@ -0,0 +1,3 @@ +[0122/125519.017:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[0122/192130.891:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[0123/134808.113:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) diff --git a/package-lock.json b/package-lock.json index f74368dd3..0eff1ff92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9296,9 +9296,9 @@ "integrity": "sha512-vTgEjKjS89C5yHL5qWPpT6BzKuOVqABp+A3Szpbx34pIy3sngxlGaFpgHhfj6fKze1w0QKeOSDbU7SKu7wDvRQ==" }, "mobx": { - "version": "5.15.4", - "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.4.tgz", - "integrity": "sha512-xRFJxSU2Im3nrGCdjSuOTFmxVDGeqOHL+TyADCGbT0k4HHqGmx5u2yaHNryvoORpI4DfbzjJ5jPmuv+d7sioFw==" + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.7.tgz", + "integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==" }, "mobx-react": { "version": "5.4.4", diff --git a/package.json b/package.json index 5c456449d..7a9ec6f99 100644 --- a/package.json +++ b/package.json @@ -185,8 +185,8 @@ "lodash": "^4.17.15", "material-ui": "^0.20.2", "mobile-detect": "^1.4.4", - "mobx": "^5.15.3", - "mobx-react": "^5.3.5", + "mobx": "^5.15.7", + "mobx-react": "^5.4.4", "mobx-react-devtools": "^6.1.1", "mobx-utils": "^5.6.1", "mongodb": "^3.5.9", diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx index 3f875057e..1d4653471 100644 --- a/src/client/ClientRecommender.tsx +++ b/src/client/ClientRecommender.tsx @@ -40,8 +40,6 @@ const fieldkey = "data"; @observer export class ClientRecommender extends React.Component<RecommenderProps> { - - static Instance: ClientRecommender; private mainDoc?: RecommenderDocument; private docVectors: Set<RecommenderDocument> = new Set(); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index ccdfeed5f..f47e198be 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -519,7 +519,7 @@ export class CurrentUserUtils { { title: "Import", target: Cast(doc.myImportPanel, Doc, null), icon: "upload", click: 'selectMainMenu(self)' }, { title: "Sharing", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc }, { title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)' }, - { title: "Filter", target: Cast(doc.myFilter, Doc, null), icon: "filter", click: 'selectMainMenu(self)' }, + // { title: "Filter", target: Cast(doc.currentFilter, Doc, null), icon: "filter", click: 'selectMainMenu(self)' }, { title: "Pres. Trails", target: Cast(doc.myPresentations, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' }, { title: "My Files", target: Cast(doc.myFilesystem, Doc, null), icon: "file", click: 'selectMainMenu(self)' }, { title: "Help", target: undefined as any, icon: "question-circle", click: 'selectMainMenu(self)' }, @@ -804,20 +804,19 @@ export class CurrentUserUtils { } } static setupFilterDocs(doc: Doc) { - // setup Recently Closed library item - doc.myFilter === undefined; - if (doc.myFilter === undefined) { - doc.myFilter = new PrefetchProxy(Docs.Create.FilterDocument({ - title: "FilterDoc", - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "none", - treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true, + // setup Filter item + doc.currentFilter === undefined; + if (doc.currentFilter === undefined) { + doc.currentFilter = Docs.Create.FilterDocument({ + title: "FilterDoc", _height: 20, + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _yPadding: 10, _forceActive: true, childDropAction: "none", + treeViewTruncateTitleWidth: 90, treeViewPreventOpen: false, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true - })); + }); + const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); + (doc.currentFilter as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); + (doc.currentFilter as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); } - const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([]); scriptContext._docFilters = scriptContext._docRangeFilters = undefined;`, { scriptContext: Doc.name }); - (doc.myFilter as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); - (doc.myFilter as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); - } static setupUserDoc(doc: Doc) { @@ -850,7 +849,7 @@ export class CurrentUserUtils { CurrentUserUtils.setupPresentations(doc); CurrentUserUtils.setupFilesystem(doc); CurrentUserUtils.setupRecentlyClosedDocs(doc); - CurrentUserUtils.setupFilterDocs(doc); + // CurrentUserUtils.setupFilterDocs(doc); CurrentUserUtils.setupUserDoc(doc); } @@ -1016,6 +1015,8 @@ export class CurrentUserUtils { doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); + doc.savedFilters = new List<Doc>(); + doc.filterDocCount = 0; this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon this.setupDocTemplates(doc); // sets up the template menu of templates this.setupImportSidebar(doc); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 68af67a3b..62d656c5d 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import Select from 'react-select'; @@ -284,8 +284,7 @@ export class GroupManager extends React.Component<{}> { placeholder="Group name" onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} /> <Select - isMulti={true} - isSearchable={true} + isMulti options={this.options} onChange={this.handleChange} placeholder={"Select users"} diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index f657e5b40..b132af035 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -66,7 +66,7 @@ export namespace SelectionManager { manager.SelectSchemaView(colSchema, document); } - const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed + const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed return manager.SelectedViews.get(doc) ? true : false; }); // computed functions, such as used in IsSelected generate errors if they're called outside of a diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index 5ca54517c..d8342ea56 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -75,8 +75,8 @@ width: 130; color: black; border-radius: 5px; - padding:7px; - + padding: 7px; + } } @@ -169,7 +169,7 @@ } } -.prefs-content{ +.prefs-content { text-align: left; } @@ -210,6 +210,7 @@ .preferences-font-controls { display: flex; justify-content: space-between; + width: 130%; } .font-select { @@ -429,11 +430,12 @@ font-size: 16px; font-weight: bold; margin-bottom: 16px; - } + } - .tab-column-title, .tab-column-content { - padding-left: 16px; - } + .tab-column-title, + .tab-column-content { + padding-left: 16px; + } } @@ -462,4 +464,4 @@ .settings-interface .settings-heading { font-size: 25; } -} +}
\ No newline at end of file diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 2aea73528..08dfb5066 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -7,7 +7,7 @@ import Select from "react-select"; import * as RequestPromise from "request-promise"; import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; -import { Cast, StrCast } from "../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util"; import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; @@ -78,7 +78,16 @@ export class SharingManager extends React.Component<{}> { @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) private populating: boolean = false; // whether the list of users is populating or not @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used - @observable private myDocAcls: boolean = false; + @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not + + // maps acl symbols to SharingPermissions + private AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit], + [AclAdmin, SharingPermissions.Admin] + ]); // private get linkVisible() { // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; @@ -156,6 +165,33 @@ export class SharingManager extends React.Component<{}> { } /** + * Shares the document with a user. + */ + setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { + const { user, sharingDoc } = recipient; + const target = targetDoc || this.targetDoc!; + const acl = `acl-${normalizeEmail(user.email)}`; + const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; + + const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); + docs.forEach(doc => { + doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc); + + if (permission === SharingPermissions.None) { + if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1; + } + else { + if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1; + } + + distributeAcls(acl, permission as SharingPermissions, doc); + + if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); + }); + } + + /** * Sets the permission on the target for the group. * @param group * @param permission @@ -170,6 +206,14 @@ export class SharingManager extends React.Component<{}> { docs.forEach(doc => { doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc); + + if (permission === SharingPermissions.None) { + if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1; + } + else { + if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1; + } + distributeAcls(acl, permission as SharingPermissions, doc); if (group instanceof Doc) { @@ -265,21 +309,21 @@ export class SharingManager extends React.Component<{}> { /** * Shares the document with a user. */ - setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { - const { user, sharingDoc } = recipient; - const target = targetDoc || this.targetDoc!; - const acl = `acl-${normalizeEmail(user.email)}`; - const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; - - const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); - docs.forEach(doc => { - doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc); - distributeAcls(acl, permission as SharingPermissions, doc); - - if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); - else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); - }); - } + // setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { + // const { user, sharingDoc } = recipient; + // const target = targetDoc || this.targetDoc!; + // const acl = `acl-${normalizeEmail(user.email)}`; + // const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`; + + // const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); + // docs.forEach(doc => { + // doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc); + // distributeAcls(acl, permission as SharingPermissions, doc); + + // if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); + // else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); + // }); + // } // private setExternalSharing = (permission: string) => { @@ -313,11 +357,11 @@ export class SharingManager extends React.Component<{}> { if (!uniform) dropdownValues.unshift("-multiple-"); if (override) dropdownValues.unshift("None"); return dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => - ( - <option key={permission} value={permission}> - {permission === SharingPermissions.Add ? "Can Augment" : permission} - </option> - ) + ( + <option key={permission} value={permission}> + {permission === SharingPermissions.Add ? "Can Augment" : permission} + </option> + ) ); } @@ -397,20 +441,12 @@ export class SharingManager extends React.Component<{}> { } distributeOverCollection = (targetDoc?: Doc) => { - const AclMap = new Map<symbol, string>([ - [AclPrivate, SharingPermissions.None], - [AclReadonly, SharingPermissions.View], - [AclAddonly, SharingPermissions.Add], - [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin] - ]); - const target = targetDoc || this.targetDoc!; const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); docs.forEach(doc => { for (const [key, value] of Object.entries(doc[AclSym])) { - distributeAcls(key, AclMap.get(value)! as SharingPermissions, target); + distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target); } }); } @@ -471,7 +507,8 @@ export class SharingManager extends React.Component<{}> { const targetDoc = docs[0]; // tslint:disable-next-line: no-unnecessary-callback-wrapper - const admin = this.myDocAcls ? Boolean(docs.length) : docs.map(doc => GetEffectiveAcl(doc)).every(acl => acl === AclAdmin); // if the user has admin access to all selected docs + const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); + const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))); @@ -535,7 +572,7 @@ export class SharingManager extends React.Component<{}> { <span className={"padding"}>Me</span> <div className="edit-actions"> <div className={"permissions-dropdown"}> - {targetDoc?.[`acl-${Doc.CurrentUserEmailNormalized}`]} + {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : "-multiple-"} </div> </div> </div> @@ -545,7 +582,7 @@ export class SharingManager extends React.Component<{}> { // the list of groups shared with const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true); - groupListMap.unshift({ title: "Public" }, { title: "Override" }); + groupListMap.unshift({ title: "Public" });//, { title: "Override" }); const groupListContents = groupListMap.map(group => { const groupKey = `acl-${StrCast(group.title)}`; const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]); @@ -597,9 +634,10 @@ export class SharingManager extends React.Component<{}> { {<div className="share-container"> <div className="share-setup"> <Select - className={"user-search"} - placeholder={"Enter user or group name..."} + className="user-search" + placeholder="Enter user or group name..." isMulti + isSearchable closeMenuOnSelect={false} options={options} onChange={this.handleUsersChange} @@ -623,16 +661,16 @@ export class SharingManager extends React.Component<{}> { </div> <div className="acl-container"> - <div className="myDocs-acls"> + {/* <div className="myDocs-acls"> <input type="checkbox" onChange={action(() => this.myDocAcls = !this.myDocAcls)} checked={this.myDocAcls} /> <label>My Docs</label> - </div> + </div> */} {Doc.UserDoc().noviceMode ? (null) : <div className="layoutDoc-acls"> <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> </div>} - <button className="distribute-button" onClick={() => this.distributeOverCollection()}> + {/* <button className="distribute-button" onClick={() => this.distributeOverCollection()}> Distribute - </button> + </button> */} </div> </div> } diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index 4a89cc69c..5953baec1 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -8,6 +8,8 @@ } .editableView-container-editing-oneLine { + width: 100%; + span { white-space: nowrap; overflow: hidden; diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index e2f171f9d..f1cecd272 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -47,6 +47,7 @@ export interface EditableProps { toggle?: () => void; background?: string | undefined; placeholder?: string; + fullWidth?: boolean; // used in PropertiesView to make the whole key:value input box clickable } /** diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 6e467efd7..20bba1299 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -28,6 +28,10 @@ import { MainView } from "./MainView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; import { AnchorMenu } from "./pdf/AnchorMenu"; import { SearchBox } from "./search/SearchBox"; +import { random } from "lodash"; +import { DocumentView } from "./nodes/DocumentView"; +import { SettingsManager } from "../util/SettingsManager"; +import { AudioBox } from "./nodes/AudioBox"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -135,6 +139,7 @@ export class KeyManager { DictationManager.Controls.stop(); GoogleAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); + if (!GroupManager.Instance.isOpen) SettingsManager.Instance.close(); GroupManager.Instance.close(); CollectionFreeFormViewChrome.Instance?.clearKeepPrimitiveMode(); window.getSelection()?.empty(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 1fbcd8fa4..c356aa0f5 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -45,6 +45,7 @@ import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { LinkMenu } from './linking/LinkMenu'; import "./MainView.scss"; +import "./collections/TreeView.scss"; import { AudioBox } from './nodes/AudioBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView, DocumentViewProps, DocAfterFocusFunc } from './nodes/DocumentView'; @@ -60,7 +61,7 @@ import { AnchorMenu } from './pdf/AnchorMenu'; import { PreviewCursor } from './PreviewCursor'; import { PropertiesView } from './PropertiesView'; import { SearchBox } from './search/SearchBox'; -import { DefaultStyleProvider, StyleProp } from './StyleProvider'; +import { DefaultStyleProvider, DashboardStyleProvider, StyleProp } from './StyleProvider'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -166,7 +167,8 @@ export class MainView extends React.Component { fa.faWindowMinimize, fa.faWindowRestore, fa.faTextWidth, fa.faTextHeight, fa.faClosedCaptioning, fa.faInfoCircle, fa.faTag, fa.faSyncAlt, fa.faPhotoVideo, fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical, fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, fa.faBorderAll, - fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines); + fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines, + fa.faSave, fa.faBookmark); this.initAuthenticationRouters(); } @@ -177,8 +179,8 @@ export class MainView extends React.Component { const targClass = targets[0].className.toString(); if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) { const check = targets.some((thing) => - (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" || - thing.className === "collectionSchema-header-menuOptions")); + (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" || + thing.className === "collectionSchema-header-menuOptions")); !check && SearchBox.Instance.resetSearch(true); } !targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu(); diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 1365725cb..134c50972 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -157,6 +157,67 @@ } } + .propertiesView-filters { + //border-bottom: 1px solid black; + //padding: 8.5px; + + .propertiesView-filters-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-filters-title-icon { + float: right; + justify-items: right; + align-items: flex-end; + margin-left: auto; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-filters-content { + font-size: 10px; + padding: 10px; + margin-left: 5px; + max-height: 40%; + overflow-y: auto; + + .propertiesView-buttonContainer { + float: right; + display: flex; + + button { + width: 15; + height: 15; + padding: 0; + margin-top: -5; + } + } + + button { + width: 5; + height: 5; + } + + input { + width: 100%; + } + } + } + + .propertiesView-appearance { //border-bottom: 1px solid black; //padding: 8.5px; @@ -332,6 +393,7 @@ } } } + .propertiesView-fields { //border-bottom: 1px solid black; //padding: 8.5px; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 73c688029..4b6f1c912 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -2,16 +2,16 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Checkbox, Tooltip } from "@material-ui/core"; import { intersection } from "lodash"; -import { action, computed, observable } from "mobx"; +import { action, autorun, computed, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, WidthSym } from "../../fields/Doc"; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkField } from "../../fields/InkField"; import { ComputedField } from "../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../fields/Types"; import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from "../../fields/util"; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse } from "../../Utils"; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../Utils"; import { DocumentType } from "../documents/DocumentTypes"; import { DocumentManager } from "../util/DocumentManager"; import { SelectionManager } from "../util/SelectionManager"; @@ -28,7 +28,10 @@ import { PresBox } from "./nodes/PresBox"; import { PropertiesButtons } from "./PropertiesButtons"; import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; import "./PropertiesView.scss"; -import { DefaultStyleProvider } from "./StyleProvider"; +import { DefaultStyleProvider, FilteringStyleProvider } from "./StyleProvider"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; +import { FilterBox } from "./nodes/FilterBox"; +import { List } from "../../fields/List"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -47,6 +50,9 @@ 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 filterDoc() { + return FilterBox._filterScope === "Current Collection" ? this.selectedDoc || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; + } @computed get selectedDocumentView() { if (SelectionManager.Views().length) return SelectionManager.Views()[0]; if (PresBox.Instance?._selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); @@ -67,6 +73,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openContexts: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; + @observable openFilters: boolean = true; // should be false + + private selectedDocListenerDisposer: Opt<Lambda>; + // @observable selectedUser: string = ""; // @observable addButtonPressed: boolean = false; @observable layoutDocAcls: boolean = false; @@ -81,6 +91,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable _controlBtn: boolean = false; @observable _lock: boolean = false; + componentDidMount() { + this.selectedDocListenerDisposer?.(); + this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc()); + } + + componentWillUnmount() { + this.selectedDocListenerDisposer?.(); + } + @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } rtfWidth = () => { @@ -149,6 +168,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { rows.push(<div className="propertiesView-field" key={"newKeyValue"} style={{ marginTop: "3px" }}> <EditableView key="editableView" + oneLine contents={"add key:value or #tags"} height={13} fontSize={10} @@ -205,6 +225,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { rows.push(<div className="propertiesView-field" key={"newKeyValue"} style={{ marginTop: "3px" }}> <EditableView key="editableView" + oneLine contents={"add key:value or #tags"} height={13} fontSize={10} @@ -398,7 +419,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs - const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym]))); + const commonKeys: string[] = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym]))); const tableEntries = []; @@ -415,7 +436,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); // shifts the current user, owner, public to the top of the doc. - tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); + // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); tableEntries.unshift(this.sharingItem("Public", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Public"] === docs[0]["acl-Public"]) ? (AclMap.get(target[AclSym]?.["acl-Public"]) || SharingPermissions.None) : "-multiple-")); tableEntries.unshift(this.sharingItem("Me", showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? "Owner" : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : "-multiple-", !ownerSame)); if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, "Owner")); @@ -442,13 +463,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const titles = new Set<string>(); SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title))); const title = Array.from(titles.keys()).length > 1 ? "--multiple selected--" : StrCast(this.selectedDoc?.title); - return <div className="editable-title"><EditableView - key="editableView" - contents={title} - height={25} - fontSize={14} - GetValue={() => title} - SetValue={this.setTitle} /> </div>; + return <div className="editable-title"> + <EditableView + key="editableView" + contents={title} + height={25} + fontSize={14} + GetValue={() => title} + SetValue={this.setTitle} /> + </div>; } @undoBatch @@ -512,8 +535,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } } - - @computed get controlPointsButton() { const formatInstance = InkStrokeProperties.Instance; @@ -829,6 +850,209 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } + @computed get optionsSubMenu() { + return <div className="propertiesView-settings" onPointerEnter={action(() => this.inOptions = true)} + onPointerLeave={action(() => this.inOptions = false)}> + <div className="propertiesView-settings-title" + onPointerDown={action(() => this.openOptions = !this.openOptions)} + style={{ backgroundColor: this.openOptions ? "black" : "" }}> + Options + <div className="propertiesView-settings-title-icon"> + <FontAwesomeIcon icon={this.openOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openOptions ? (null) : + <div className="propertiesView-settings-content"> + <PropertiesButtons /> + </div>} + </div>; + } + + @computed get sharingSubMenu() { + return <div className="propertiesView-sharing"> + <div className="propertiesView-sharing-title" + onPointerDown={action(() => this.openSharing = !this.openSharing)} + style={{ backgroundColor: this.openSharing ? "black" : "" }}> + Sharing {"&"} Permissions + <div className="propertiesView-sharing-title-icon"> + <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openSharing ? (null) : + <div className="propertiesView-sharing-content"> + <div className="propertiesView-buttonContainer"> + {!Doc.UserDoc().noviceMode ? (<div className="propertiesView-acls-checkbox"> + <Checkbox + color="primary" + onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} + checked={this.layoutDocAcls} + /> + <div className="propertiesView-acls-checkbox-text">Layout</div> + </div>) : (null)} + {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> + <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> + <FontAwesomeIcon icon="redo-alt" color="white" size="1x" /> + </button> + </Tooltip> */} + </div> + {this.sharingTable} + </div>} + </div>; + } + + checkFilterDoc() { + if (this.filterDoc.type === DocumentType.COL && !this.filterDoc.currentFilter) CurrentUserUtils.setupFilterDocs(this.filterDoc); + } + + createNewFilterDoc = () => { + const temp = this.filterDoc._docFilters; + this.filterDoc._docFilters = new List<string>(); + (this.filterDoc.currentFilter as Doc)._docFiltersList = temp; + this.filterDoc.currentFilter = undefined; + CurrentUserUtils.setupFilterDocs(this.filterDoc); + } + + updateFilterDoc = (doc: Doc) => { + const temp = doc._docFiltersList; + const otherTemp = this.filterDoc._docFilters; + this.filterDoc._docFilters = new List<string>(); + (this.filterDoc.currentFilter as Doc)._docFiltersList = otherTemp; + this.filterDoc.currentFilter = doc; + doc._docFiltersList = new List<string>(); + this.filterDoc._docFilters = temp; + } + + @computed get filtersSubMenu() { + return <div className="propertiesView-filters"> + <div className="propertiesView-filters-title" + onPointerDown={action(() => this.openFilters = !this.openFilters)} + style={{ backgroundColor: this.openFilters ? "black" : "" }}> + Filters + <div className="propertiesView-filters-title-icon"> + <FontAwesomeIcon icon={this.openFilters ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + { + !this.openFilters || !this.filterDoc.currentFilter ? (null) : + <div className="propertiesView-filters-content"> + <DocumentView + Document={this.filterDoc.currentFilter as Doc} + DataDoc={undefined} + addDocument={undefined} + addDocTab={returnFalse} + pinToPres={emptyFunction} + rootSelected={returnTrue} + removeDocument={returnFalse} + ScreenToLocalTransform={this.getTransform} + PanelWidth={this.docWidth} + PanelHeight={this.docHeight} + renderDepth={0} + scriptContext={this.filterDoc.currentFilter as Doc} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + createNewFilterDoc={this.createNewFilterDoc} + updateFilterDoc={this.updateFilterDoc} + docViewPath={returnEmptyDoclist} + layerProvider={undefined} + /> + </div> + } + </div >; + } + + @computed get inkSubMenu() { + return <> + {!this.isInk ? (null) : + <div className="propertiesView-appearance"> + <div className="propertiesView-appearance-title" + onPointerDown={action(() => this.openAppearance = !this.openAppearance)} + style={{ backgroundColor: this.openAppearance ? "black" : "" }}> + Appearance + <div className="propertiesView-appearance-title-icon"> + <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!this.openAppearance ? (null) : + <div className="propertiesView-appearance-content"> + {this.appearanceEditor} + </div>} + </div>} + + {this.isInk ? <div className="propertiesView-transform"> + <div className="propertiesView-transform-title" + onPointerDown={action(() => this.openTransform = !this.openTransform)} + style={{ backgroundColor: this.openTransform ? "black" : "" }}> + Transform + <div className="propertiesView-transform-title-icon"> + <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openTransform ? <div className="propertiesView-transform-content"> + {this.transformEditor} + </div> : null} + </div> : null} + </>; + } + + @computed get fieldsSubMenu() { + return <div className="propertiesView-fields"> + <div className="propertiesView-fields-title" + onPointerDown={action(() => this.openFields = !this.openFields)} + style={{ backgroundColor: this.openFields ? "black" : "" }}> + Fields {"&"} Tags + <div className="propertiesView-fields-title-icon"> + <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {!Doc.UserDoc().noviceMode && this.openFields ? <div className="propertiesView-fields-checkbox"> + {this.fieldsCheckbox} + <div className="propertiesView-fields-checkbox-text">Layout</div> + </div> : null} + {!this.openFields ? (null) : + <div className="propertiesView-fields-content"> + {Doc.UserDoc().noviceMode ? this.noviceFields : this.expandedField} + </div>} + </div>; + } + + @computed get contextsSubMenu() { + return <div className="propertiesView-contexts"> + <div className="propertiesView-contexts-title" + onPointerDown={action(() => this.openContexts = !this.openContexts)} + style={{ backgroundColor: this.openContexts ? "black" : "" }}> + Contexts + <div className="propertiesView-contexts-title-icon"> + <FontAwesomeIcon icon={this.openContexts ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openContexts ? <div className="propertiesView-contexts-content" >{this.contexts}</div> : null} + </div>; + } + + @computed get layoutSubMenu() { + return <div className="propertiesView-layout"> + <div className="propertiesView-layout-title" + onPointerDown={action(() => this.openLayout = !this.openLayout)} + style={{ backgroundColor: this.openLayout ? "black" : "" }}> + Layout + <div className="propertiesView-layout-title-icon"> + <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openLayout ? <div className="propertiesView-layout-content" >{this.layoutPreview}</div> : null} + </div>; + } + + + /** * Handles adding and removing members from the sharing panel */ @@ -849,8 +1073,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } else { - const novice = Doc.UserDoc().noviceMode; - if (this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width, @@ -863,121 +1085,19 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-name"> {this.editableTitle} </div> - <div className="propertiesView-settings" onPointerEnter={action(() => this.inOptions = true)} - onPointerLeave={action(() => this.inOptions = false)}> - <div className="propertiesView-settings-title" - onPointerDown={action(() => this.openOptions = !this.openOptions)} - style={{ backgroundColor: this.openOptions ? "black" : "" }}> - Options - <div className="propertiesView-settings-title-icon"> - <FontAwesomeIcon icon={this.openOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openOptions ? (null) : - <div className="propertiesView-settings-content"> - <PropertiesButtons /> - </div>} - </div> - <div className="propertiesView-sharing"> - <div className="propertiesView-sharing-title" - onPointerDown={action(() => this.openSharing = !this.openSharing)} - style={{ backgroundColor: this.openSharing ? "black" : "" }}> - Sharing {"&"} Permissions - <div className="propertiesView-sharing-title-icon"> - <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openSharing ? (null) : - <div className="propertiesView-sharing-content"> - <div className="propertiesView-buttonContainer"> - {!novice ? (<div className="propertiesView-acls-checkbox"> - <Checkbox - color="primary" - onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} - checked={this.layoutDocAcls} - /> - <div className="propertiesView-acls-checkbox-text">Layout</div> - </div>) : (null)} - <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}> - <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> - <FontAwesomeIcon icon="redo-alt" color="white" size="1x" /> - </button> - </Tooltip> - </div> - {this.sharingTable} - </div>} - </div> + {this.optionsSubMenu} - {!this.isInk ? (null) : - <div className="propertiesView-appearance"> - <div className="propertiesView-appearance-title" - onPointerDown={action(() => this.openAppearance = !this.openAppearance)} - style={{ backgroundColor: this.openAppearance ? "black" : "" }}> - Appearance - <div className="propertiesView-appearance-title-icon"> - <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!this.openAppearance ? (null) : - <div className="propertiesView-appearance-content"> - {this.appearanceEditor} - </div>} - </div>} + {this.sharingSubMenu} - {this.isInk ? <div className="propertiesView-transform"> - <div className="propertiesView-transform-title" - onPointerDown={action(() => this.openTransform = !this.openTransform)} - style={{ backgroundColor: this.openTransform ? "black" : "" }}> - Transform - <div className="propertiesView-transform-title-icon"> - <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openTransform ? <div className="propertiesView-transform-content"> - {this.transformEditor} - </div> : null} - </div> : null} + {this.selectedDoc.type === DocumentType.COL && this.filtersSubMenu} - <div className="propertiesView-fields"> - <div className="propertiesView-fields-title" - onPointerDown={action(() => this.openFields = !this.openFields)} - style={{ backgroundColor: this.openFields ? "black" : "" }}> - Fields {"&"} Tags - <div className="propertiesView-fields-title-icon"> - <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {!novice && this.openFields ? <div className="propertiesView-fields-checkbox"> - {this.fieldsCheckbox} - <div className="propertiesView-fields-checkbox-text">Layout</div> - </div> : null} - {!this.openFields ? (null) : - <div className="propertiesView-fields-content"> - {novice ? this.noviceFields : this.expandedField} - </div>} - </div> - <div className="propertiesView-contexts"> - <div className="propertiesView-contexts-title" - onPointerDown={action(() => this.openContexts = !this.openContexts)} - style={{ backgroundColor: this.openContexts ? "black" : "" }}> - Contexts - <div className="propertiesView-contexts-title-icon"> - <FontAwesomeIcon icon={this.openContexts ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openContexts ? <div className="propertiesView-contexts-content" >{this.contexts}</div> : null} - </div> - <div className="propertiesView-layout"> - <div className="propertiesView-layout-title" - onPointerDown={action(() => this.openLayout = !this.openLayout)} - style={{ backgroundColor: this.openLayout ? "black" : "" }}> - Layout - <div className="propertiesView-layout-title-icon"> - <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openLayout ? <div className="propertiesView-layout-content" >{this.layoutPreview}</div> : null} - </div> + {this.inkSubMenu} + + {this.fieldsSubMenu} + + {this.contextsSubMenu} + + {this.layoutSubMenu} </div>; } if (this.isPres) { diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 2352aa22a..91667fd64 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -15,6 +15,8 @@ import { MainView } from './MainView'; import { DocumentViewProps, DocumentView } from "./nodes/DocumentView"; import { FieldViewProps } from './nodes/FieldView'; import "./StyleProvider.scss"; +import "./collections/TreeView.scss"; +import "./nodes/FilterBox.scss"; import React = require("react"); import Color = require('color'); @@ -23,6 +25,7 @@ export enum StyleLayers { } export enum StyleProp { + TreeViewIcon = "treeViewIcon", DocContents = "docContents", // when specified, the JSX returned will replace the normal rendering of the document view Opacity = "opacity", // opacity of the document view Hidden = "hidden", // whether the document view should not be isplayed @@ -71,6 +74,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); switch (property.split(":")[0]) { + case StyleProp.TreeViewIcon: return doc ? Doc.toIcon(doc) : "question"; case StyleProp.DocContents: return undefined; case StyleProp.WidgetColor: return isAnnotated ? "lightBlue" : darkScheme() ? "lightgrey" : "dimgrey"; case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null)); @@ -164,6 +168,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | if (doc?.type !== DocumentType.INK && layer === true) return "all"; return undefined; case StyleProp.Decorations: + // if (isFooter) + if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform) { return doc && (isBackground() || selected) && (props?.renderDepth || 0) > 0 && ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? @@ -175,6 +181,85 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | } } + +function toggleHidden(e: React.MouseEvent, doc: Doc) { + UndoManager.RunInBatch(() => runInAction(() => { + e.stopPropagation(); + doc.hidden = doc.hidden ? undefined : true; + }), "toggleHidden"); +} + +function toggleLock(e: React.MouseEvent, doc: Doc) { + UndoManager.RunInBatch(() => runInAction(() => { + e.stopPropagation(); + doc.lockedPosition = doc.lockedPosition ? undefined : true; + }), "toggleHidden"); +} + +/** + * add lock and hide button decorations for the "Dashboards" flyout TreeView + */ +export function DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { + switch (property.split(":")[0]) { + case StyleProp.Decorations: + if (doc) { + const hidden = doc.hidden; + const locked = doc.lockedPosition; + return doc._viewType === CollectionViewType.Docking || (Doc.IsSystem(doc) && Doc.UserDoc().noviceMode) ? (null) : + <> + <div className={`styleProvider-treeView-hide${hidden ? "-active" : ""}`} onClick={(e) => toggleHidden(e, doc)}> + <FontAwesomeIcon icon={hidden ? "eye-slash" : "eye"} size="sm" /> + </div> + <div className={`styleProvider-treeView-lock${locked ? "-active" : ""}`} onClick={(e) => toggleLock(e, doc)}> + <FontAwesomeIcon icon={locked ? "lock" : "unlock"} size="sm" /> + </div> + </>; + } + default: return DefaultStyleProvider(doc, props, property); + + } +} + +function changeFilterBool(e: any, doc: Doc) { + UndoManager.RunInBatch(() => runInAction(() => { + //e.stopPropagation(); + //doc.lockedPosition = doc.lockedPosition ? undefined : true; + }), "changeFilterBool"); +} + +function closeFilter(e: React.MouseEvent, doc: Doc) { + UndoManager.RunInBatch(() => runInAction(() => { + e.stopPropagation(); + //doc.lockedPosition = doc.lockedPosition ? undefined : true; + }), "closeFilter"); +} + + +/** + * add (to treeView) for filtering decorations + */ +export function FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { + switch (property.split(":")[0]) { + case StyleProp.Decorations: + if (doc) { + return doc._viewType === CollectionViewType.Docking || (Doc.IsSystem(doc)) ? (null) : + <> + <div> + <select className="filterBox-treeView-selection" onChange={e => changeFilterBool(e, doc)}> + <option value="Is" key="Is">Is</option> + <option value="Is Not" key="Is Not">Is Not</option> + </select> + </div> + <div className="filterBox-treeView-close" onClick={(e) => closeFilter(e, doc)}> + <FontAwesomeIcon icon={"times"} size="sm" /> + </div> + </>; + } + default: return DefaultStyleProvider(doc, props, property); + + } +} + // // a preliminary semantic-"layering/grouping" mechanism for determining interactive properties of documents // currently, the provider tests whether the docuemnt's layer field matches the activeLayer field of the tab. diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 7527a0c5a..b14949d47 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -35,7 +35,8 @@ export type collectionTreeViewProps = { }; @observer -export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) { +export class + CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) { private treedropDisposer?: DragManager.DragDropDisposer; private _isChildActive = false; private _mainEle?: HTMLDivElement; @@ -219,7 +220,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll render() { TraceMobx(); if (!(this.doc instanceof Doc)) return (null); - const background = this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); + const background = this.props.treeViewHideTitle && this.props.treeViewHideHeaderFields ? "#9F9F9F" : this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); const paddingX = `${NumCast(this.doc._xPadding, 15)}px`; const paddingTop = `${NumCast(this.doc._yPadding, 20)}px`; const pointerEvents = !this.props.active() && !SnappingManager.GetIsDragging() && !this._isChildActive ? "none" : undefined; diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 067675038..2f74a49bb 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -8,6 +8,7 @@ width: 100%; overflow: hidden; } + .treeView-container, .treeView-container-active { .bullet-outline { @@ -20,21 +21,26 @@ .treeView-bulletIcons { width: 15px; + .treeView-expandIcon { display: none; left: -10px; position: absolute; } + .treeView-checkIcon { - left: -10px; + left: 3.5px; + top: 2px; position: absolute; } + &:hover { .treeView-expandIcon { display: unset; } } } + .bullet { position: relative; width: $TREE_BULLET_WIDTH; @@ -45,9 +51,11 @@ border-radius: 4px; } } + .treeView-container-active { z-index: 100; position: relative; + .formattedTextbox-sidebar { background-color: #ffff001f !important; height: 500px !important; @@ -70,7 +78,8 @@ display: flex; overflow: hidden; } -.treeView-border{ + +.treeView-border { border-left: dashed 1px #00000042; } @@ -78,15 +87,20 @@ .treeView-header { border: transparent 1px solid; display: flex; + //align-items: center; ::-webkit-scrollbar { - display: none; + display: none; } + .formattedTextBox-cont { - .formattedTextbox-sidebar, .formattedTextbox-sidebar-inking { + + .formattedTextbox-sidebar, + .formattedTextbox-sidebar-inking { overflow: visible !important; border-left: unset; } + overflow: visible !important; } @@ -104,12 +118,18 @@ margin-left: 0.25rem; opacity: 0.75; - >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide, .styleProvider-treeView-lock-active, .styleProvider-treeView-hide-active { + >svg, + .styleProvider-treeView-lock, + .styleProvider-treeView-hide, + .styleProvider-treeView-lock-active, + .styleProvider-treeView-hide-active { margin-left: 0.25rem; - margin-right: 0.25rem; + margin-right: 0.25rem; } - - >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide { + + >svg, + .styleProvider-treeView-lock, + .styleProvider-treeView-hide { display: none; } } @@ -134,7 +154,10 @@ } .right-buttons-container { - >svg, .styleProvider-treeView-lock, .styleProvider-treeView-hide { + + >svg, + .styleProvider-treeView-lock, + .styleProvider-treeView-hide { display: inherit; } } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 106129127..58bb5974f 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -446,6 +446,7 @@ export class TreeView extends React.Component<TreeViewProps> { TraceMobx(); const iconType = this.doc.isFolder ? (this.treeViewOpen ? "chevron-down" : "chevron-right") : Doc.toIcon(this.doc); const checked = this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; + return <div className={`bullet${this.outlineMode ? "-outline" : ""}`} key={"bullet"} title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} onClick={this.bulletClick} @@ -469,7 +470,6 @@ export class TreeView extends React.Component<TreeViewProps> { } </div>; } - @computed get headerElements() { return (Doc.IsSystem(this.doc) && Doc.UserDoc().noviceMode) || this.props.treeViewHideHeaderFields() ? (null) : <> @@ -627,7 +627,7 @@ export class TreeView extends React.Component<TreeViewProps> { {view} </div > <div className={"right-buttons-container"}> - {this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations + (Doc.IsSystem(this.props.containingCollection) ? ":afterHeader" : ""))} {/* hide and lock buttons */} + {this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations)} {/* hide and lock buttons */} {this.headerElements} </div> </>; diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 749ffa7fd..36572b861 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -3,6 +3,7 @@ .documentView-effectsWrapper { border-radius: inherit; } + .documentView-node, .documentView-node-topmost { position: inherit; @@ -37,12 +38,13 @@ overflow-y: scroll; height: calc(100% - 20px); } + .documentView-linkAnchorBoxAnchor { - display:flex; + display: flex; overflow: hidden; .documentView-node { - width:10px !important; + width: 10px !important; } } @@ -64,6 +66,7 @@ top: 15%; } } + .documentView-treeView { max-height: 1.5em; text-overflow: ellipsis; @@ -71,7 +74,8 @@ white-space: pre; width: 100%; overflow: hidden; - > .documentView-node { + + >.documentView-node { position: absolute; } } @@ -80,14 +84,33 @@ border-radius: inherit; width: 100%; height: 100%; + + .sharingIndicator { + height: 30px; + width: 30px; + border-radius: 50%; + position: absolute; + right: -15; + opacity: 0.9; + pointer-events: auto; + background-color: #9dca96; + letter-spacing: 2px; + font-size: 10px; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } } .documentView-anchorCont { position: absolute; - top: 0; - left: 0; + top: 0; + left: 0; width: 100%; - height: 100%; + height: 100%; display: inline-block; pointer-events: none; } @@ -100,6 +123,7 @@ top: 0; left: 0; } + .documentView-styleWrapper { position: absolute; display: inline-block; @@ -113,7 +137,8 @@ position: absolute; } - .documentView-titleWrapper, .documentView-titleWrapper-hover { + .documentView-titleWrapper, + .documentView-titleWrapper-hover { overflow: hidden; color: white; transform-origin: top left; @@ -126,8 +151,9 @@ white-space: pre; position: absolute; } + .documentView-titleWrapper-hover { - display:none; + display: none; } .documentView-searchHighlight { @@ -150,18 +176,21 @@ } -.documentView-node:hover, .documentView-node-topmost:hover { - > .documentView-styleWrapper { - > .documentView-titleWrapper-hover { - display:inline-block; +.documentView-node:hover, +.documentView-node-topmost:hover { + >.documentView-styleWrapper { + >.documentView-titleWrapper-hover { + display: inline-block; } } - > .documentView-styleWrapper { - > .documentView-captionWrapper { + + >.documentView-styleWrapper { + >.documentView-captionWrapper { opacity: 1; } } } + .contentFittingDocumentView { position: relative; display: flex; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a85082f26..83022a759 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -10,7 +10,7 @@ import { listSpec } from "../../../fields/Schema"; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { AudioField } from "../../../fields/URLField"; -import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; +import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; import { emptyFunction, hasDescendantTarget, OmitKeys, returnFalse, returnVal, Utils } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; @@ -119,6 +119,8 @@ export interface DocumentViewSharedProps { cantBrush?: boolean; // whether the document doesn't show brush highlighting pointerEvents?: string; scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document + createNewFilterDoc?: () => void; + updateFilterDoc?: (doc: Doc) => void; } export interface DocumentViewProps extends DocumentViewSharedProps { // properties specific to DocumentViews but not to FieldView @@ -602,7 +604,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.props.Document === CurrentUserUtils.ActiveDashboard) { alert((e.target as any)?.closest?.("*.lm_content") ? "You can't perform this move most likely because you don't have permission to modify the destination." : - "linking to document tabs not yet supported. Drop link on document content."); + "Linking to document tabs not yet supported. Drop link on document content."); return; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; @@ -799,6 +801,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div>; } + get indicatorIcon() { + if (this.props.Document["acl-Public"] !== SharingPermissions.None) return "globe-americas"; + else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return "users"; + else return "user"; + } + @undoBatch hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true) anchorPanelWidth = () => this.props.PanelWidth() || 1; diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/nodes/FilterBox.scss index d32cc0d2b..4fe1fa2eb 100644 --- a/src/client/views/nodes/FilterBox.scss +++ b/src/client/views/nodes/FilterBox.scss @@ -1,17 +1,155 @@ - - .filterBox-flyout { - width: 400px; display: block; text-align: left; + font-weight: 100; + .filterBox-flyout-facet { - background-color: lightgray; - text-align: left; - display: inline-block; - position: relative; - width: 100%; + background-color: white; + text-align: left; + display: inline-block; + position: relative; + width: 100%; + + .filterBox-flyout-facet-check { + margin-right: 6px; + } + } +} + + +.filter-bookmark { + //display: flex; + + .filter-bookmark-icon { + float: right; + margin-right: 10px; + margin-top: 7px; } } + +// .filterBox-bottom { +// // position: fixed; +// // bottom: 0; +// // width: 100%; +// } + +.filterBox-select { + // width: 90%; + margin-top: 5px; + // margin-bottom: 15px; +} + + +.filterBox-saveBookmark { + background-color: #e9e9e9; + border-radius: 11px; + padding-left: 8px; + padding-right: 8px; + padding-top: 5px; + padding-bottom: 5px; + margin: 8px; + display: flex; + font-size: 11px; + cursor: pointer; + + &:hover { + background-color: white; + } + + .filterBox-saveBookmark-icon { + margin-right: 6px; + margin-top: 4px; + margin-left: 2px; + } + +} + +.filterBox-select-scope, +.filterBox-select-bool, +.filterBox-addWrapper, +.filterBox-select-matched, +.filterBox-saveWrapper { + font-size: 10px; + justify-content: center; + justify-items: center; + padding-bottom: 10px; + display: flex; +} + +.filterBox-addWrapper { + font-size: 11px; + width: 100%; +} + +.filterBox-saveWrapper { + width: 100%; +} + +// .filterBox-top { +// padding-bottom: 20px; +// border-bottom: 2px solid black; +// position: fixed; +// top: 0; +// width: 100%; +// } + +.filterBox-select-scope { + padding-bottom: 20px; + border-bottom: 2px solid black; +} + +.filterBox-title { + font-size: 15; + // border: 2px solid black; + width: 100%; + align-self: center; + text-align: center; + background-color: #d3d3d3; +} + +.filterBox-select-bool { + margin-top: 6px; +} + +.filterBox-select-text { + margin-right: 8px; + margin-left: 8px; + margin-top: 3px; +} + +.filterBox-select-box { + margin-right: 2px; + font-size: 30px; + border: 0; + background: transparent; +} + +.filterBox-selection { + border-radius: 6px; + border: none; + background-color: #e9e9e9; + padding: 2px; + + &:hover { + background-color: white; + } +} + +.filterBox-addFilter { + width: 120px; + background-color: #e9e9e9; + border-radius: 12px; + padding: 5px; + margin: 5px; + display: flex; + text-align: center; + justify-content: center; + + &:hover { + background-color: white; + } +} + .filterBox-treeView { display: flex; flex-direction: column; @@ -20,24 +158,19 @@ position: absolute; right: 0; top: 0; - border-left: solid 1px; z-index: 1; + background-color: #9F9F9F; .filterBox-addfacet { display: inline-block; width: 200px; height: 30px; - background: darkGray; text-align: left; .filterBox-addFacetButton { display: flex; margin: auto; cursor: pointer; - - .filterBox-span { - margin-right: 15px; - } } >div, @@ -50,6 +183,7 @@ .filterBox-tree { display: inline-block; width: 100%; - height: calc(100% - 30px); + margin-bottom: 10px; + //height: calc(100% - 30px); } }
\ No newline at end of file diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index bdd9334f4..a4ae0b5a3 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -1,14 +1,14 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { computed } from "mobx"; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { List } from "../../../fields/List"; import { RichTextField } from "../../../fields/RichTextField"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast } from "../../../fields/Types"; +import { Cast, StrCast } from "../../../fields/Types"; import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnZero, returnTrue } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -20,23 +20,52 @@ import { FieldView, FieldViewProps } from './FieldView'; import './FilterBox.scss'; import { Scripting } from "../../util/Scripting"; import { values } from "lodash"; +import { tokenToString } from "typescript"; +import { SelectionManager } from "../../util/SelectionManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; +import Select from "react-select"; +import { UserOptions } from "../../util/GroupManager"; +import { DocumentViewProps } from "./DocumentView"; +import { DefaultStyleProvider, StyleProp } from "../StyleProvider"; +import { CollectionViewType } from "../collections/CollectionView"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; type FilterBoxDocument = makeInterface<[typeof documentSchema]>; const FilterBoxDocument = makeInterface(documentSchema); @observer export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDocument>(FilterBoxDocument) { + + static Instance: FilterBox; + + constructor(props: Readonly<FieldViewProps>) { + super(props); + FilterBox.Instance = this; + const targetDoc = FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; + if (!targetDoc) CurrentUserUtils.setupFilterDocs(targetDoc); + } public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterBox, fieldKey); } + public _filterBoolean = "AND"; + @observable public static _filterScope = "Current Dashboard"; + public _filterSelected = false; + public _filterMatch = "matched"; + + @observable private showFilterDialog = false; + + @computed get targetDoc() { + return FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; + } + @computed get allDocs() { const allDocs = new Set<Doc>(); - if (CollectionDockingView.Instance) { - const activeTabs = DocListCast(CollectionDockingView.Instance.props.Document.data); + const targetDoc = FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; + if (targetDoc) { + const activeTabs = DocListCast(targetDoc.data); SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allDocs.add(doc)); - setTimeout(() => CollectionDockingView.Instance.props.Document.allDocuments = new List<Doc>(Array.from(allDocs))); + setTimeout(() => targetDoc.allDocuments = new List<Doc>(Array.from(allDocs))); } return allDocs; } @@ -51,6 +80,18 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc return Array.from(keys.keys()).filter(key => key[0]).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("_")) || noviceFields.includes(key) || !Doc.UserDoc().noviceMode).sort(); } + + /** + * The current attributes selected to filter based on + */ + @computed get activeAttributes() { + return DocListCast(this.dataDoc[this.props.fieldKey]); + } + + @computed get currentFacets() { + return this.activeAttributes.map(attribute => StrCast(attribute.title)); + } + gatherFieldValues(dashboard: Doc, facetKey: string) { const childDocs = DocListCast((dashboard.data as any)[0].data); const valueSet = new Set<string>(); @@ -83,28 +124,59 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc }); return { strings: Array.from(valueSet.keys()), rtFields }; } - /** - * Responds to clicking the check box in the flyout menu - */ - facetClick = (facetHeader: string) => { - const targetDoc = CollectionDockingView.Instance.props.Document; - const found = DocListCast(this.dataDoc[this.props.fieldKey]).findIndex(doc => doc.title === facetHeader); + + public removeFilter = (filterName: string) => { + console.log("remove filter"); + const targetDoc = FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; + const filterDoc = targetDoc.currentFilter as Doc; + const attributes = DocListCast(filterDoc["data"]); + const found = attributes.findIndex(doc => doc.title === filterName); if (found !== -1) { - (this.dataDoc[this.props.fieldKey] as List<Doc>).splice(found, 1); + (filterDoc["data"] as List<Doc>).splice(found, 1); const docFilter = Cast(targetDoc._docFilters, listSpec("string")); if (docFilter) { let index: number; - while ((index = docFilter.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + while ((index = docFilter.findIndex(item => item.split(":")[0] === filterName)) !== -1) { docFilter.splice(index, 1); } } const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string")); if (docRangeFilters) { let index: number; - while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === filterName)) !== -1) { docRangeFilters.splice(index, 3); } } + } + } + + /** + * Responds to clicking the check box in the flyout menu + */ + facetClick = (facetHeader: string) => { + + console.log("facetClick: " + facetHeader); + console.log(this.props.fieldKey); + + const targetDoc = FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; + // const targetDoc = SelectionManager.Views()[0].props.Document; + const found = this.activeAttributes.findIndex(doc => doc.title === facetHeader); + if (found !== -1) { + // (this.dataDoc[this.props.fieldKey] as List<Doc>).splice(found, 1); + // const docFilter = Cast(targetDoc._docFilters, listSpec("string")); + // if (docFilter) { + // let index: number; + // while ((index = docFilter.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + // docFilter.splice(index, 1); + // } + // } + // const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string")); + // if (docRangeFilters) { + // let index: number; + // while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) { + // docRangeFilters.splice(index, 3); + // } + // } } else { const allCollectionDocs = DocListCast((targetDoc.data as any)[0].data); const facetValues = this.gatherFieldValues(targetDoc, facetHeader); @@ -158,26 +230,123 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); return script ? () => script : undefined; } + + @action + changeBool = (e: any) => { + this._filterBoolean = e.currentTarget.value; + console.log(this._filterBoolean); + } + + @action + changeScope = (e: any) => { + FilterBox._filterScope = e.currentTarget.value; + console.log(FilterBox._filterScope); + } + + @action + changeMatch = (e: any) => { + this._filterMatch = e.currentTarget.value; + console.log(this._filterMatch); + } + @action + changeSelected = () => { + if (this._filterSelected) { + this._filterSelected = false; + SelectionManager.DeselectAll(); + } else { + this._filterSelected = true; + // helper method to select specified docs + } + console.log(this._filterSelected); + } + + FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { + switch (property.split(":")[0]) { + case StyleProp.Decorations: + if (doc) { + return doc._viewType === CollectionViewType.Docking || (Doc.IsSystem(doc)) ? (null) : + <> + <div style={{ marginRight: "5px", fontSize: "10px" }}> + <select className="filterBox-selection"> + <option value="Is" key="Is">Is</option> + <option value="Is Not" key="Is Not">Is Not</option> + </select> + </div> + <div className="filterBox-treeView-close" onClick={e => this.removeFilter(StrCast(doc.title))}> + <FontAwesomeIcon icon={"times"} size="sm" /> + </div> + </>; + } + default: return DefaultStyleProvider(doc, props, property); + + } + } + suppressChildClick = () => ScriptField.MakeScript("")!; + + saveFilter = () => { + // const doc: Doc = new Doc; + // for (const key of Object.keys(this.props.Document)) { + // doc[key] = SerializationHelper.Serialize(this.props.Document[key] as Field); + // } + // console.log(doc); + runInAction(() => this.showFilterDialog = true); + console.log("saved filter"); + } + + onTitleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.props.Document.title = e.currentTarget.value || `FilterDoc for ${SelectionManager.Views()[0].Document.title}`; + } + + onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + runInAction(() => this.showFilterDialog = false); + Doc.AddDocToList(Doc.UserDoc(), "savedFilters", this.props.Document); + } + } + render() { const facetCollection = this.props.Document; - const flyout = <div className="filterBox-flyout" style={{ width: `100%`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}> - {this._allFacets.map(facet => <label className="filterBox-flyout-facet" key={`${facet}`} onClick={e => this.facetClick(facet)}> - <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey]).some(d => d.title === facet)} /> - <span className="checkmark" /> - {facet} - </label>)} - </div>; + + const flyout = DocListCast(Doc.UserDoc().savedFilters).map(doc => { + // console.log("mapping"); + return <> + <div className="???" onWheel={e => e.stopPropagation()} style={{ height: 50, border: "2px" }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}> + {StrCast(doc.title)} + </div> + </>; + } + ); + + const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); return this.props.dontRegisterView ? (null) : <div className="filterBox-treeView" style={{ width: "100%" }}> - <div className="filterBox-addFacet" style={{ width: "100%" }} onPointerDown={e => e.stopPropagation()}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <div className="filterBox-addFacetButton"> - <FontAwesomeIcon icon={"edit"} size={"lg"} /> - <span className="filterBox-span">Choose Facets</span> - </div> - </Flyout> + + <div className="filterBox-title">Current FilterDoc: {this.props.Document.title}</div> + <div className="filterBox-select-bool"> + <select className="filterBox-selection" onChange={e => this.changeBool(e)}> + <option value="AND" key="AND">AND</option> + <option value="OR" key="OR">OR</option> + </select> + <div className="filterBox-select-text">filters in </div> + <select className="filterBox-selection" onChange={e => this.changeScope(e)}> + <option value="Current Dashboard" key="Current Dashboard" selected={"Current Dashboard" === FilterBox._filterScope}>Current Dashboard</option> + {/* <option value="Current Tab" key="Current Tab">Current Tab</option> */} + <option value="Current Collection" key="Current Collection" selected={"Current Collection" === FilterBox._filterScope}>Current Collection</option> + </select> + </div> + + <div className="filterBox-select"> + <Select + placeholder="Add a filter..." + options={options} + isMulti={false} + onChange={val => this.facetClick((val as UserOptions).value)} + value={null} + closeMenuOnSelect={false} + /> </div> + <div className="filterBox-tree" key="tree"> <CollectionTreeView Document={facetCollection} @@ -219,6 +388,55 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc removeDocument={returnFalse} addDocument={returnFalse} /> </div> + {/* <Flyout className="filterBox-flyout" anchorPoint={anchorPoints.RIGHT_TOP} content={flyout}> + <div className="filterBox-addWrapper"> + <div className="filterBox-addFilter"> + add a filter</div> + </div> + </Flyout> */} + <div className="filterBox-bottom"> + <div className="filterBox-select-matched"> + <input className="filterBox-select-box" type="checkbox" + onChange={this.changeSelected} /> + <div className="filterBox-select-text">select</div> + <select className="filterBox-selection" onChange={e => this.changeMatch(e)}> + <option value="matched" key="matched">matched</option> + <option value="unmatched" key="unmatched">unmatched</option> + </select> + <div className="filterBox-select-text">documents</div> + </div> + + <div style={{ display: "flex" }}> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark" + onPointerDown={this.saveFilter} + > + <div>SAVE</div> + </div> + </div> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark"> + <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={flyout}> + <div>FILTERS</div> + </Flyout> + </div> + </div> + <div className="filterBox-saveWrapper"> + <div className="filterBox-saveBookmark" + onPointerDown={this.props.createNewFilterDoc} + > + <div>NEW</div> + </div> + </div> + </div> + {!this.showFilterDialog ? (null) : + <input + className="filterBox-dialog-input" + placeholder="Enter name of filterDoc" + onChange={this.onTitleValueChange} + onKeyDown={this.onKeyDown} + /> + } + </div> </div>; } } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 05acaf39d..c2f5efadc 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -25,6 +25,7 @@ import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLField"; import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; import JSZip = require("jszip"); +import { FilterBox } from "../client/views/nodes/FilterBox"; import { prefix } from "@fortawesome/free-regular-svg-icons"; export namespace Field { @@ -1068,7 +1069,7 @@ export namespace Doc { // all documents with the specified value for the specified key are included/excluded // based on the modifiers :"check", "x", undefined export function setDocFilter(target: Opt<Doc>, key: string, value: any, modifiers?: "remove" | "match" | "check" | "x" | undefined) { - const container = target ?? CollectionDockingView.Instance.props.Document; + const container = target ?? FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document; const docFilters = Cast(container._docFilters, listSpec("string"), []); runInAction(() => { for (let i = 0; i < docFilters.length; i++) { |