import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import { Doc, DocListCast } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { Utils, emptyFunction } from "../../../Utils"; import { DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; import { undoBatch } from "../../util/UndoManager"; import { EditableView } from "../EditableView"; import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; import { DocLike } from "../MetadataEntryMenu"; import { CollectionViewType } from "./CollectionBaseView"; import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; import * as Autosuggest from 'react-autosuggest'; import KeyRestrictionRow from "./KeyRestrictionRow"; const datepicker = require('js-datepicker'); interface CollectionViewChromeProps { CollectionView: CollectionView; type: CollectionViewType; collapse?: (value: boolean) => any; } interface Filter { key: string; value: string; contains: boolean; } let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); @observer export class CollectionViewBaseChrome extends React.Component { //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) _templateCommand = { title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined }; _contentCommand = { // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting... title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List(draggedDocs.map((d: any) => Doc.MakeAlias(d))) }; _viewCommand = { title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; }, initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; } }; _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; _masonry_commands = [this._contentCommand, this._templateCommand]; _tree_commands = []; private get _buttonizableCommands() { switch (this.props.type) { case CollectionViewType.Tree: return this._tree_commands; case CollectionViewType.Stacking: return this._stacking_commands; case CollectionViewType.Masonry: return this._stacking_commands; case CollectionViewType.Freeform: return this._freeform_commands; } return []; } private _picker: any; private _commandRef = React.createRef(); private _autosuggestRef = React.createRef(); @observable private _currentKey: string = ""; @observable private _viewSpecsOpen: boolean = false; @observable private _dateWithinValue: string = ""; @observable private _dateValue: Date | string = ""; @observable private _keyRestrictions: [JSX.Element, string][] = []; @observable private suggestions: string[] = []; @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); } getFilters = (script: string) => { let re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g; let arr: any[] = re.exec(script); let toReturn: Filter[] = []; if (arr !== null) { let filter: Filter = { key: arr[2], value: arr[3], contains: (arr[1] === "!") ? false : true, }; toReturn.push(filter); script = script.replace(arr[0], ""); if (re.exec(script) !== null) { toReturn.push(...this.getFilters(script)); } else { return toReturn; } } return toReturn; } addKeyRestrictions = (fields: Filter[]) => { if (fields.length !== 0) { for (let i = 0; i < fields.length; i++) { this._keyRestrictions.push([ runInAction(() => this._keyRestrictions[i][1] = value)} />, ""]); } if (this._keyRestrictions.length === 1) { this._keyRestrictions.push([ runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]); } } else { this._keyRestrictions.push([ runInAction(() => this._keyRestrictions[0][1] = value)} />, ""]); this._keyRestrictions.push([ runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]); } } componentDidMount = () => { let fields: Filter[] = []; if (this.filterValue) { let string = this.filterValue.script.originalScript; fields = this.getFilters(string); } runInAction(() => { this.addKeyRestrictions(fields); // chrome status is one of disabled, collapsed, or visible. this determines initial state from document let chromeStatus = this.props.CollectionView.props.Document.chromeStatus; if (chromeStatus) { if (chromeStatus === "disabled") { throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); } else if (chromeStatus === "collapsed") { if (this.props.collapse) { this.props.collapse(true); } } } }); } @undoBatch viewChanged = (e: React.ChangeEvent) => { //@ts-ignore this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value); } @action openViewSpecs = (e: React.SyntheticEvent) => { this._viewSpecsOpen = true; //@ts-ignore if (!e.target.classList[0].startsWith("qs")) { this.closeDatePicker(); } e.stopPropagation(); document.removeEventListener("pointerdown", this.closeViewSpecs); document.addEventListener("pointerdown", this.closeViewSpecs); } @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); }; @action openDatePicker = (e: React.PointerEvent) => { this.openViewSpecs(e); if (this._picker) { this._picker.alwaysShow = true; this._picker.show(); // TODO: calendar is offset when zoomed in/out // this._picker.calendar.style.position = "absolute"; // let transform = this.props.CollectionView.props.ScreenToLocalTransform(); // let x = parseInt(this._picker.calendar.style.left) / transform.Scale; // let y = parseInt(this._picker.calendar.style.top) / transform.Scale; // this._picker.calendar.style.left = x; // this._picker.calendar.style.top = y; e.stopPropagation(); } } @action addKeyRestriction = (e: React.MouseEvent) => { let index = this._keyRestrictions.length; this._keyRestrictions.push([ runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]); this.openViewSpecs(e); } @action.bound applyFilter = (e: React.MouseEvent) => { this.openViewSpecs(e); let keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; let weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0; let dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7; let dateRestrictionScript = ""; if (this._dateValue instanceof Date) { let lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset); let upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1); dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; } else { let createdDate = new Date(this._dateValue); if (!isNaN(createdDate.getTime())) { let lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset); let upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1); dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; } } let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : `return (${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : "return true"; let compiled = CompileScript(fullScript, { params: { doc: Doc.name }, typecheck: false }); if (compiled.compiled) { this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled); } } @action closeDatePicker = () => { if (this._picker) { this._picker.alwaysShow = false; this._picker.hide(); } document.removeEventListener("pointerdown", this.closeDatePicker); } @action toggleCollapse = () => { this.props.CollectionView.props.Document.chromeStatus = this.props.CollectionView.props.Document.chromeStatus === "enabled" ? "collapsed" : "enabled"; if (this.props.collapse) { this.props.collapse(this.props.CollectionView.props.Document.chromeStatus !== "enabled"); } } subChrome = () => { switch (this.props.type) { case CollectionViewType.Stacking: return (); case CollectionViewType.Schema: return (); case CollectionViewType.Tree: return (); default: return null; } } private get document() { return this.props.CollectionView.props.Document; } private get pivotKey() { return StrCast(this.document.pivotField); } private set pivotKey(value: string) { this.document.pivotField = value; } @observable private pivotKeyDisplay = this.pivotKey; getPivotInput = () => { if (!this.document.usePivotLayout) { return (null); } return () => this.pivotKeyDisplay = e.currentTarget.value)} onKeyPress={action((e: React.KeyboardEvent) => { let value = e.currentTarget.value; if (e.which === 13) { this.pivotKey = value; this.pivotKeyDisplay = ""; } })} />); } @action.bound clearFilter = () => { let compiled = CompileScript("return true", { params: { doc: Doc.name }, typecheck: false }); if (compiled.compiled) { this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled); } this._keyRestrictions = []; this.addKeyRestrictions([]); } private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer && this.dropDisposer(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } } @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.data instanceof DragManager.DocumentDragData && de.data.draggedDocuments.length) { this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.data.draggedDocuments)); e.stopPropagation(); } return true; } datePickerRef = (node: HTMLInputElement) => { if (node) { try { this._picker = datepicker("#" + node.id, { disabler: (date: Date) => date > new Date(), onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date), dateSelected: new Date() }); } catch (e) { console.log("date picker exception:" + e); } } } renderSuggestion = (suggestion: string) => { return

{suggestion}

; } getSuggestionValue = (suggestion: string) => suggestion; @action onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { this._currentKey = newValue; } onSuggestionFetch = async ({ value }: { value: string }) => { const sugg = await this.getKeySuggestions(value); runInAction(() => this.suggestions = sugg); } @action onSuggestionClear = () => { this.suggestions = []; } getKeySuggestions = async (value: string): Promise => { return this._buttonizableCommands.filter(c => c.title.indexOf(value) !== -1).map(c => c.title); } autoSuggestDown = (e: React.PointerEvent) => { e.stopPropagation(); } private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; private _sensitivity: number = 16; dragCommandDown = (e: React.PointerEvent) => { this._startDragPosition = { x: e.clientX, y: e.clientY }; document.addEventListener("pointermove", this.dragPointerMove); document.addEventListener("pointerup", this.dragPointerUp); e.stopPropagation(); e.preventDefault(); } dragPointerMove = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); let [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y]; if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY)); document.removeEventListener("pointermove", this.dragPointerMove); document.removeEventListener("pointerup", this.dragPointerUp); } } dragPointerUp = (e: PointerEvent) => { document.removeEventListener("pointermove", this.dragPointerMove); document.removeEventListener("pointerup", this.dragPointerUp); } render() { let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled"; return (
{ }} onPointerDown={this.openViewSpecs} id="viewSpecsInput" /> {this.getPivotInput()}
{this._keyRestrictions.map(i => i[0])}
CREATED WITHIN:
runInAction(() => this._dateValue = e.target.value)} onPointerDown={this.openDatePicker} placeholder="Value" />
{this.subChrome()}
); } } @observer export class CollectionStackingViewChrome extends React.Component { @observable private _currentKey: string = ""; @observable private suggestions: string[] = []; @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); } @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); } getKeySuggestions = async (value: string): Promise => { value = value.toLowerCase(); let docs: Doc | Doc[] | Promise | Promise | (() => DocLike) = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]); if (typeof docs === "function") { docs = docs(); } docs = await docs; if (docs instanceof Doc) { return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); } else { const keys = new Set(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); } } @action onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { this._currentKey = newValue; } getSuggestionValue = (suggestion: string) => suggestion; renderSuggestion = (suggestion: string) => { return

{suggestion}

; } onSuggestionFetch = async ({ value }: { value: string }) => { const sugg = await this.getKeySuggestions(value); runInAction(() => { this.suggestions = sugg; }); } @action onSuggestionClear = () => { this.suggestions = []; } setValue = (value: string) => { this.props.CollectionView.props.Document.sectionFilter = value; return true; } @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; }; @action resetValue = () => { this._currentKey = this.sectionFilter; }; render() { return (
GROUP ITEMS BY:
this.sectionFilter} autosuggestProps={ { resetValue: this.resetValue, value: this._currentKey, onChange: this.onKeyChange, autosuggestProps: { inputProps: { value: this._currentKey, onChange: this.onKeyChange }, getSuggestionValue: this.getSuggestionValue, suggestions: this.suggestions, alwaysRenderSuggestions: true, renderSuggestion: this.renderSuggestion, onSuggestionsFetchRequested: this.onSuggestionFetch, onSuggestionsClearRequested: this.onSuggestionClear } }} oneLine SetValue={this.setValue} contents={this.sectionFilter ? this.sectionFilter : "N/A"} />
); } } @observer export class CollectionSchemaViewChrome extends React.Component { // private _textwrapAllRows: boolean = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; @undoBatch togglePreview = () => { let dividerWidth = 4; let borderWidth = Number(COLLECTION_BORDER_WIDTH); let panelWidth = this.props.CollectionView.props.PanelWidth(); let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); let tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth; this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0; } @undoBatch @action toggleTextwrap = async () => { let textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []); if (textwrappedRows.length) { this.props.CollectionView.props.Document.textwrappedSchemaRows = new List([]); } else { let docs: Doc | Doc[] | Promise | Promise | (() => DocLike) = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]); if (typeof docs === "function") { docs = docs(); } docs = await docs; if (docs instanceof Doc) { let allRows = [docs[Id]]; this.props.CollectionView.props.Document.textwrappedSchemaRows = new List(allRows); } else { let allRows = docs.map(doc => doc[Id]); this.props.CollectionView.props.Document.textwrappedSchemaRows = new List(allRows); } } } render() { let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); let textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; return (
Wrap Text:
{textWrapped ? "on" : "off"}
Show Preview:
{previewWidth !== 0 ? "on" : "off"}
); } } @observer export class CollectionTreeViewChrome extends React.Component { @observable private _currentKey: string = ""; @observable private suggestions: string[] = []; @computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); } @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); } getKeySuggestions = async (value: string): Promise => { value = value.toLowerCase(); let docs: Doc | Doc[] | Promise | Promise | (() => DocLike) = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]); if (typeof docs === "function") { docs = docs(); } docs = await docs; if (docs instanceof Doc) { return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); } else { const keys = new Set(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); } } @action onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { this._currentKey = newValue; } getSuggestionValue = (suggestion: string) => suggestion; renderSuggestion = (suggestion: string) => { return

{suggestion}

; } onSuggestionFetch = async ({ value }: { value: string }) => { const sugg = await this.getKeySuggestions(value); runInAction(() => { this.suggestions = sugg; }); } @action onSuggestionClear = () => { this.suggestions = []; } setValue = (value: string) => { this.props.CollectionView.props.Document.sectionFilter = value; return true; } @action toggleSort = () => { if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined; else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false; else this.props.CollectionView.props.Document.sortAscending = true; } @action resetValue = () => { this._currentKey = this.sectionFilter; }; render() { return (
); } }