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 { undoBatch } from "../../util/UndoManager"; import { EditableView } from "../EditableView"; import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; import { CollectionViewType } from "./CollectionView"; 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; } const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); @observer export class CollectionViewBaseChrome extends React.Component { //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) _templateCommand = { title: "=> item view", script: "setChildLayout(this.target, this.source?.[0])", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) }; _narrativeCommand = { title: "=> click item view", script: "setChildDetailedLayout(this.target, this.source?.[0])", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.setChildDetailedLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) }; _contentCommand = { title: "=> 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: "=> saved view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], 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; }, immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document._panX = 0; this.props.CollectionView.props.Document._panY = 0; this.props.CollectionView.props.Document.scale = 1; }, }; _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; _masonry_commands = [this._contentCommand, this._templateCommand]; _schema_commands = [this._templateCommand, this._narrativeCommand]; _tree_commands = []; private get _buttonizableCommands() { switch (this.props.type) { case CollectionViewType.Tree: return this._tree_commands; case CollectionViewType.Schema: return this._schema_commands; case CollectionViewType.Stacking: return this._stacking_commands; case CollectionViewType.Masonry: return this._stacking_commands; case CollectionViewType.Freeform: return this._freeform_commands; case CollectionViewType.Time: return this._freeform_commands; case CollectionViewType.Carousel: 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) => { const re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g; const arr: any[] = re.exec(script); const toReturn: Filter[] = []; if (arr !== null) { const 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) { const 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 const 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); } commandChanged = (e: React.ChangeEvent) => { //@ts-ignore runInAction(() => this._currentKey = e.target.selectedOptions[0].value); } @action openViewSpecs = (e: React.SyntheticEvent) => { if (this._viewSpecsOpen) this.closeViewSpecs(); else { 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) => { const 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); const keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; const yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; const monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; const weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0; const dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7; let dateRestrictionScript = ""; if (this._dateValue instanceof Date) { const lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset); const 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 { const createdDate = new Date(this._dateValue); if (!isNaN(createdDate.getTime())) { const lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset); const 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()})`; } } const fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : "true"; this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name }); } @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 (); case CollectionViewType.Masonry: return (); default: return null; } } private get document() { return this.props.CollectionView.props.Document; } @action.bound clearFilter = () => { this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name }); this._keyRestrictions = []; this.addKeyRestrictions([]); } private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer && this.dropDisposer(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); } } @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.complete.docDragData && de.complete.docDragData.draggedDocuments.length) { this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData?.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(); const [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() { const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; return (
{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(); const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); 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 = () => { const dividerWidth = 4; const borderWidth = Number(COLLECTION_BORDER_WIDTH); const panelWidth = this.props.CollectionView.props.PanelWidth(); const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth; this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0; } @undoBatch @action toggleTextwrap = async () => { const textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []); if (textwrappedRows.length) { this.props.CollectionView.props.Document.textwrappedSchemaRows = new List([]); } else { const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); this.props.CollectionView.props.Document.textwrappedSchemaRows = new List(allRows); } } render() { const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); const textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; return (
Show Preview:
{previewWidth !== 0 ? "on" : "off"}
); } } @observer export class CollectionTreeViewChrome extends React.Component { get dataExtension() { return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "_ext"] as Doc; } @computed private get descending() { return this.dataExtension && Cast(this.dataExtension.sortAscending, "boolean", null); } @action toggleSort = () => { if (this.dataExtension) { if (this.dataExtension.sortAscending) this.dataExtension.sortAscending = undefined; else if (this.dataExtension.sortAscending === undefined) this.dataExtension.sortAscending = false; else this.dataExtension.sortAscending = true; } } render() { return (
); } }