import React = require('react'); import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@material-ui/core'; import { intersection } from 'lodash'; import { action, computed, Lambda, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; import { Doc, Field, FieldResult, HierarchyMapping, NumListCast, Opt, StrListCast } from '../../fields/Doc'; import { AclAdmin, DocAcl, DocData, Height, Width } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ComputedField } from '../../fields/ScriptField'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from '../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { DocumentManager } from '../util/DocumentManager'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { SharingManager } from '../util/SharingManager'; import { Transform } from '../util/Transform'; import { undoable, undoBatch, UndoManager } from '../util/UndoManager'; import { EditableView } from './EditableView'; import { FilterPanel } from './FilterPanel'; import { Colors } from './global/globalEnums'; import { InkStrokeProperties } from './InkStrokeProperties'; import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView'; import { KeyValueBox } from './nodes/KeyValueBox'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; import { PropertiesButtons } from './PropertiesButtons'; import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector'; import { PropertiesDocContextSelector } from './PropertiesDocContextSelector'; import './PropertiesView.scss'; import { DefaultStyleProvider } from './StyleProvider'; import { Button, EditableText, NumberDropdown, Size, Slider, Type } from 'browndash-components'; import { PropertiesSection } from './PropertiesSection'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { width: number; height: number; styleProvider?: StyleProviderFunc; addDocTab: (doc: Doc, where: OpenWhere) => boolean; } @observer export class PropertiesView extends React.Component { private _widthUndo?: UndoManager.Batch; @computed get MAX_EMBED_HEIGHT() { return 200; } @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard; } @computed get selectedDocumentView() { if (SelectionManager.Views().length) return SelectionManager.Views()[0]; if (PresBox.Instance?.selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); return undefined; } @computed get isPres(): boolean { return this.selectedDoc?.type === DocumentType.PRES; } @computed get isLink(): boolean { return this.selectedDoc?.type === DocumentType.LINK; } @computed get dataDoc() { return this.selectedDoc?.[DocData]; } @observable layoutFields: boolean = false; @observable openOptions: boolean = true; @observable openSharing: boolean = true; @observable openFields: boolean = false; @observable openLayout: boolean = false; @observable openContexts: boolean = true; @observable openLinks: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; @observable openFilters: boolean = false; /** * autorun to set up the filter doc of a collection if that collection has been selected and the filters panel is open */ private selectedDocListenerDisposer: Opt; // @observable selectedUser: string = ""; // @observable addButtonPressed: boolean = false; @observable layoutDocAcls: boolean = false; //Pres Trails booleans: @observable openPresTransitions: boolean = true; @observable openPresProgressivize: boolean = false; @observable openPresVisibilityAndDuration: boolean = false; @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; @observable inOptions: boolean = false; @observable _controlButton: 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 = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[Width](), this.props.width - 20)); rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[Width]() ? Math.min(this.selectedDoc?.[Height](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT); @action docWidth = () => { if (this.selectedDoc) { const layoutDoc = this.selectedDoc; const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._layout_fitWidth); if (aspect) return Math.min(layoutDoc[Width](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20)); return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[Width](), this.props.width - 20) : this.props.width - 20; } return 0; }; @action docHeight = () => { if (this.selectedDoc && this.dataDoc) { const layoutDoc = this.selectedDoc; return Math.max( 70, Math.min( this.MAX_EMBED_HEIGHT, Doc.NativeAspect(layoutDoc, undefined, true) ? this.docWidth() / Doc.NativeAspect(layoutDoc, undefined, true) : layoutDoc._layout_fitWidth ? !Doc.NativeHeight(this.dataDoc) ? NumCast(this.props.height) : Math.min((this.docWidth() * Doc.NativeHeight(layoutDoc)) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height)) : NumCast(layoutDoc._height) || 50 ) ); } return 0; }; editableFields = (filter: (key: string) => boolean, reqdKeys: string[]) => { const rows: JSX.Element[] = []; if (this.dataDoc && this.selectedDoc) { const ids = new Set(reqdKeys); const docs: Doc[] = SelectionManager.Views().length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => Object.keys(doc).forEach(key => doc[key] !== ComputedField.undefined && ids.add(key))); // prettier-ignore Array.from(ids).filter(filter).sort().map(key => { const multiple = Array.from(docs.reduce((set,doc) => set.add(doc[key]), new Set()).keys()).length > 1; const editableContents = multiple ? '-multiple-' : Field.toKeyValueString(docs[0], key); const displayContents = multiple ? '-multiple-' : Field.toString(docs[0][key] as Field); const contentElement = ( editableContents} SetValue={(value: string) => { value !== '-multiple-' && docs.map(doc => KeyValueBox.SetField(doc, key, value, true)); return true; }} />); rows.push(
{key + ':'}   {contentElement}
); }); rows.push(
''} SetValue={this.setKeyValue} />
); } return rows; }; @computed get expandedField() { return this.editableFields(returnTrue, []); } @computed get noviceFields() { const noviceReqFields = ['author', 'author_date', 'tags', '_layout_curPage']; return this.editableFields(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl')), noviceReqFields); } @undoBatch setKeyValue = (value: string) => { const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc!] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => { if (value.indexOf(':') !== -1) { const newVal = value[0].toUpperCase() + value.substring(1, value.length); const splits = newVal.split(':'); KeyValueBox.SetField(doc, splits[0], splits[1], true); const tags = StrCast(doc.tags, ':'); if (tags.includes(`${splits[0]}:`) && splits[1] === 'undefined') { KeyValueBox.SetField(doc, 'tags', `"${tags.replace(splits[0] + ':', '')}"`, true); } return true; } else if (value[0] === '#') { const tags = StrListCast(doc.tags); if (!tags.includes(value)) { tags.push(value); doc[DocData].tags = tags.length ? new List(tags) : undefined; } return true; } }); return false; }; @observable transform: Transform = Transform.Identity(); getTransform = () => this.transform; propertiesDocViewRef = (ref: HTMLDivElement) => { const observer = new _global.ResizeObserver( action((entries: any) => { const cliRect = ref.getBoundingClientRect(); this.transform = new Transform(-cliRect.x, -cliRect.y, 1); }) ); ref && observer.observe(ref); }; @computed get contexts() { return !this.selectedDoc ? null : ; } @computed get links() { const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc; return !selAnchor ? null : ; } @computed get layoutPreview() { if (SelectionManager.Views().length > 1) { return '-- multiple selected --'; } if (this.selectedDoc) { const layoutDoc = Doc.Layout(this.selectedDoc); const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes('FormattedTextBox') ? this.rtfHeight : this.docHeight; const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes('FormattedTextBox') ? this.rtfWidth : this.docWidth; return (
); } else { return null; } } /** * Handles the changing of a user's permissions from the permissions panel. */ @undoBatch changePermissions = (e: any, user: string) => { const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc! : DocCast(doc)[DocData])); SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); }; /** * @returns the options for the permissions dropdown. */ getPermissionsSelect(user: string, permission: string) { const dropdownValues: string[] = Object.values(SharingPermissions); if (permission === '-multiple-') dropdownValues.unshift(permission); if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); return ( ); } /** * @returns the notification icon. On clicking, it should notify someone of a document been shared with them. */ @computed get notifyIcon() { return ( Notify with message}>
); } /** * ... next to the owner that opens the main SharingManager interface on click. */ @computed get expansionIcon() { return ( Show more permissions}>
{ if (this.selectedDocumentView || this.selectedDoc) { SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc); } }}>
); } /** * @returns a row of the permissions panel */ sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { return (
this.selectedUser = this.selectedUser === name ? "" : name)} >
{' '} {name}{' '}
{/* {name !== "Me" ? this.notifyIcon : null} */}
{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission) : permission} {permission === 'Owner' || showExpansionIcon ? this.expansionIcon : null}
); } /** * @returns the sharing and permissions panel. */ @computed get sharingTable() { // all selected docs const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutDocAcls ? this.selectedDoc : this.dataDoc!] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DocData])); const target = docs[0]; // tslint:disable-next-line: no-unnecessary-callback-wrapper const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs const commonKeys: string[] = intersection(...docs.map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl]).filter(key => key !== 'acl-Me'))); const tableEntries = []; // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { if (commonKeys.length) { for (const key of commonKeys) { const name = denormalizeEmail(key.substring(4)); const uniform = docs.every(doc => doc?.[DocAcl]?.[key] === docs[0]?.[DocAcl]?.[key]); if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) { tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[DocAcl][key])!.name : '-multiple-')); } } } const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); // shifts the current user, owner, public to the top of the doc. // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); tableEntries.unshift(this.sharingItem('Public', showAdmin, StrCast(docs.filter(doc => doc).every(doc => doc['acl-Public'] === target['acl-Public']) ? target['acl-Public'] || SharingPermissions.None : '-multiple-'))); tableEntries.unshift( this.sharingItem( 'Me', showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-', !ownerSame ) ); return
{tableEntries}
; } @computed get fieldsCheckbox() { return ; } @action toggleCheckbox = () => (this.layoutFields = !this.layoutFields); @computed get color() { return StrCast(Doc.UserDoc().userColor); } @computed get backgroundColor() { return StrCast(Doc.UserDoc().userBackgroundColor); } @computed get variantColor() { return StrCast(Doc.UserDoc().userVariantColor); } @computed get editableTitle() { const titles = new Set(); const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title); SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title))); return ( ); } @computed get type() { const type = StrCast(this.selectedDoc?.type); return ( */} {this.sharingTable} } isOpen={this.openSharing} setIsOpen={(bool) => this.openSharing = bool} /> } /** * Updates this.filterDoc's currentFilter and saves the childFilters on the currentFilter */ updateFilterDoc = (doc: Doc) => { if (this.selectedDoc) { if (doc === this.selectedDoc.currentFilter) return; // causes problems if you try to reapply the same doc const savedDocFilters = doc._childFiltersList; const currentDocFilters = this.selectedDoc._childFilters; this.selectedDoc._childFilters = new List(); (this.selectedDoc.currentFilter as Doc)._childFiltersList = currentDocFilters; this.selectedDoc.currentFilter = doc; doc._childFiltersList = new List(); this.selectedDoc._childFilters = savedDocFilters; const savedDocRangeFilters = doc._childFiltersByRangesList; const currentDocRangeFilters = this.selectedDoc._childFiltersByRanges; this.selectedDoc._childFiltersByRanges = new List(); (this.selectedDoc.currentFilter as Doc)._childFiltersByRangesList = currentDocRangeFilters; this.selectedDoc.currentFilter = doc; doc._childFiltersByRangesList = new List(); this.selectedDoc._childFiltersByRanges = savedDocRangeFilters; } }; @computed get filtersSubMenu() { return } isOpen={this.openFilters} setIsOpen={(bool) => this.openFilters = bool} /> } @computed get inkSubMenu() { return ( <> this.openAppearance = bool} /> this.openTransform = bool} /> ); } @computed get fieldsSubMenu() { return { Doc.noviceMode ? this.noviceFields : this.expandedField} } isOpen={this.openFields} setIsOpen={(bool) => this.openFields = bool} /> } @computed get contextsSubMenu() { return this.openContexts = bool} /> } @computed get linksSubMenu() { return this.openLinks = bool} /> } @computed get layoutSubMenu() { return this.openLayout = bool} /> } @computed get description() { return Field.toString(LinkManager.currentLink?.link_description as any as Field); } @computed get relationship() { return StrCast(LinkManager.currentLink?.link_relationship); } @observable private relationshipButtonColor: string = ''; // @action // handleDescriptionChange = (e: React.ChangeEvent) => { this.link_description = e.target.value; } // handleRelationshipChange = (e: React.ChangeEvent) => { this.link_relationship = e.target.value; } handleDescriptionChange = undoable( action((value: string) => { if (LinkManager.currentLink && this.selectedDoc) { this.setDescripValue(value); } }), 'change link description' ); handlelinkRelationshipChange = undoable( action((value: string) => { if (LinkManager.currentLink && this.selectedDoc) { this.setlinkRelationshipValue(value); } }), 'change link relationship' ); @undoBatch setDescripValue = action((value: string) => { if (LinkManager.currentLink) { Doc.GetProto(LinkManager.currentLink).link_description = value; } }); @undoBatch setlinkRelationshipValue = action((value: string) => { if (LinkManager.currentLink) { const prevRelationship = StrCast(LinkManager.currentLink.link_relationship); LinkManager.currentLink.link_relationship = value; Doc.GetProto(LinkManager.currentLink).link_relationship = value; const linkRelationshipList = StrListCast(Doc.UserDoc().link_relationshipList); const linkRelationshipSizes = NumListCast(Doc.UserDoc().link_relationshipSizes); const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color if (!linkRelationshipList?.includes(value)) { linkRelationshipList.push(value); linkRelationshipSizes.push(1); const randColor = 'rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')'; linkColorList.push(randColor); // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes } else if (linkRelationshipList && value !== prevRelationship) { const index = linkRelationshipList.indexOf(value); //increment size of new relationship size if (index !== -1 && index < linkRelationshipSizes.length) { const pvalue = linkRelationshipSizes[index]; linkRelationshipSizes[index] = pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1; } //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation) if (linkRelationshipList.includes(prevRelationship)) { const pindex = linkRelationshipList.indexOf(prevRelationship); if (pindex !== -1 && pindex < linkRelationshipSizes.length) { const pvalue = linkRelationshipSizes[pindex]; linkRelationshipSizes[pindex] = Math.max(0, pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1); } } } this.relationshipButtonColor = 'rgb(62, 133, 55)'; setTimeout( action(() => (this.relationshipButtonColor = '')), 750 ); return true; } }); changeFollowBehavior = undoable((loc: Opt) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior'); @undoBatch changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior)); @undoBatch changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimDirection = effect)); animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { const lanch = this.sourceAnchor; const color = lanch?.followLinkAnimDirection === direction || (direction === PresEffectDirection.Center && !lanch?.followLinkAnimDirection) ? Colors.MEDIUM_BLUE : ''; return ( {direction}}>
this.changeEffectDirection(direction)}> {icon ? : null}
); }; onSelectOutDesc = () => { this.setDescripValue(this.description); document.getElementById('link_description_input')?.blur(); }; onDescriptionKey = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { this.setDescripValue(this.description); document.getElementById('link_description_input')?.blur(); } }; onSelectOutRelationship = () => { this.setlinkRelationshipValue(this.relationship); document.getElementById('link_relationship_input')?.blur(); }; onRelationshipKey = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { this.setlinkRelationshipValue(this.relationship); document.getElementById('link_relationship_input')?.blur(); } }; toggleLinkProp = (e: React.PointerEvent, prop: string) => { setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop])))); }; @computed get destinationAnchor() { const ldoc = LinkManager.currentLink; const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch; return ldoc ? DocCast(ldoc.link_anchor_2) : ldoc; } @computed get sourceAnchor() { const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink); } toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: any = true, ovalue: any = false, cb: (val: any) => any = val => val) => { anchor && setupMoveUpEvents( this, e, returnFalse, emptyFunction, undoBatch( action(() => { anchor[prop] = anchor[prop] === value ? ovalue : value; this.selectedDoc && cb(anchor[prop]); }) ) ); }; @computed get editRelationship() { return ( this.handlelinkRelationshipChange(e.currentTarget.value)} className="text" type="text" /> ); } @computed get editDescription() { return (