import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Popup, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import Select from 'react-select'; import { StopEvent, Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { RichTextField } from '../../../../fields/RichTextField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { FInfo, FInfoFieldType } from '../../../documents/Documents'; import { DocFocusOrOpen } from '../../../util/DocumentManager'; import { dropActionType } from '../../../util/DragManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch, undoable } from '../../../util/UndoManager'; import { EditableView } from '../../EditableView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DefaultStyleProvider } from '../../StyleProvider'; import { Colors } from '../../global/globalEnums'; import { OpenWhere, returnEmptyDocViewList } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { ColumnType, FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; export interface SchemaTableCellProps { Document: Doc; col: number; deselectCell: () => void; selectCell: (doc: Doc, col: number, shift: boolean, ctrl: boolean) => void; selectedCells: () => Doc[] | undefined; selectedCol: () => number; fieldKey: string; maxWidth?: () => number; columnWidth: () => number; rowHeight: () => number; padding?: number; // default is 5 -- see scss isRowActive: () => boolean | undefined; getFinfo: (fieldKey: string) => FInfo | undefined; setColumnValues: (field: string, value: string) => boolean; setSelectedColumnValues: (field: string, value: string) => boolean; oneLine?: boolean; // whether all input should fit on one line vs allowing textare multiline inputs allowCRs?: boolean; // allow carriage returns in text input (othewrise CR ends the edit) finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView) options?: string[]; menuTarget: HTMLDivElement | null; transform: () => Transform; autoFocus?: boolean; // whether to set focus on creation, othwerise wait for a click rootSelected?: () => boolean; } @observer export class SchemaTableCell extends ObservableReactComponent { constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } static addFieldDoc = (doc: Doc, where: OpenWhere) => { DocFocusOrOpen(doc); return true; }; public static renderProps(props: SchemaTableCellProps) { const { Document, fieldKey, getFinfo, columnWidth, isRowActive } = props; let protoCount = 0; let doc: Doc | undefined = Document; while (doc) { if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) { break; } protoCount++; doc = DocCast(doc.proto); } const parenCount = Math.max(0, protoCount - 1); const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; //color of text in cells const textDecoration = color !== 'black' && parenCount ? 'underline' : ''; const fieldProps: FieldViewProps = { childFilters: returnEmptyFilter, childFiltersByRanges: returnEmptyFilter, docViewPath: returnEmptyDocViewList, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, dragAction: dropActionType.move, renderDepth: 1, noSidebar: true, isContentActive: returnFalse, whenChildContentsActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, addDocTab: SchemaTableCell.addFieldDoc, pinToPres: returnZero, Document: DocCast(Document.rootDocument, Document), fieldKey: fieldKey, PanelWidth: columnWidth, PanelHeight: props.rowHeight, rootSelected: props.rootSelected, }; const readOnly = getFinfo(fieldKey)?.readOnly ?? false; const cursor = !readOnly ? 'text' : 'default'; const pointerEvents: 'all' | 'none' = !readOnly && isRowActive() ? 'all' : 'none'; return { color, textDecoration, fieldProps, cursor, pointerEvents }; } @computed get selected() { const selectedDocs: Doc[] | undefined = this._props.selectedCells(); let isSelected = this._props.isRowActive() && selectedDocs?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col; return isSelected; } @computed get defaultCellContent() { const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props); return (
this.selected && this._props.autoFocus && r?.setIsFocused(true)} oneLine={this._props.oneLine} allowCRs={this._props.allowCRs} contents={undefined} fieldContents={fieldProps} editing={this.selected ? undefined : false} GetValue={() => Field.toKeyValueString(fieldProps.Document, this._props.fieldKey, SnappingManager.MetaKey)} SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value); this._props.finishEdit?.(); return true; } const ret = KeyValueBox.SetField(fieldProps.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(fieldProps.Document) ? true : undefined); this._props.finishEdit?.(); return ret; }, 'edit schema cell')} />
); } get getCellType() { const columnTypeStr = this._props.getFinfo(this._props.fieldKey)?.fieldType; const cellValue = this._props.Document[this._props.fieldKey]; if (cellValue instanceof ImageField) return ColumnType.Image; if (cellValue instanceof DateField) return ColumnType.Date; if (cellValue instanceof RichTextField) return ColumnType.RTF; if (typeof cellValue === 'number') return ColumnType.Any; if (typeof cellValue === 'string' && columnTypeStr !== FInfoFieldType.enumeration) return ColumnType.Any; if (typeof cellValue === 'boolean') return ColumnType.Boolean; if (columnTypeStr && columnTypeStr in FInfotoColType) { return FInfotoColType[columnTypeStr]; } return ColumnType.Any; } get content() { const cellType: ColumnType = this.getCellType; // prettier-ignore switch (cellType) { case ColumnType.Image: return ; case ColumnType.Boolean: return ; case ColumnType.RTF: return ; case ColumnType.Enumeration: return val.toString())} />; case ColumnType.Date: return ; default: return this.defaultCellContent; } } render() { return (
StopEvent(e)} onPointerDown={action(e => { const shift: boolean = e.shiftKey; const ctrl: boolean = e.ctrlKey; if (this._props.isRowActive?.() !== false) { if (this.selected && ctrl) { this._props.selectCell(this._props.Document, this._props.col, shift, ctrl); e.stopPropagation(); } else !this.selected && this._props.selectCell(this._props.Document, this._props.col, shift, ctrl); } })} style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}> {this.content}
); } } // mj: most of this is adapted from old schema code so I'm not sure what it does tbh @observer export class SchemaImageCell extends ObservableReactComponent { constructor(props: any) { super(props); makeObservable(this); } @observable _previewRef: HTMLImageElement | undefined = undefined; choosePath(url: URL) { if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question const ext = extname(url.href); return url.href.replace(ext, '_s' + ext); } get url() { const field = Cast(this._props.Document[this._props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this._props.Document[this._props.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images const altpaths = alts .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) .filter(url => url) .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; // If there is a path, follow it; otherwise, follow a link to a default image icon const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')]; return url[0]; } @action showHoverPreview = (e: React.PointerEvent) => { this._previewRef = document.createElement('img'); document.body.appendChild(this._previewRef); const ext = extname(this.url); this._previewRef.src = this.url.replace('_s' + ext, '_m' + ext); this._previewRef.style.position = 'absolute'; this._previewRef.style.left = e.clientX + 10 + 'px'; this._previewRef.style.top = e.clientY + 10 + 'px'; this._previewRef.style.zIndex = '1000'; }; @action moveHoverPreview = (e: React.PointerEvent) => { if (!this._previewRef) return; this._previewRef.style.left = e.clientX + 10 + 'px'; this._previewRef.style.top = e.clientY + 10 + 'px'; }; @action removeHoverPreview = (e: React.PointerEvent) => { if (!this._previewRef) return; document.body.removeChild(this._previewRef); }; render() { const aspect = Doc.NativeAspect(this._props.Document); // aspect ratio // let width = Math.max(75, this._props.columnWidth); // get a with that is no smaller than 75px // const height = Math.max(75, width / aspect); // get a height either proportional to that or 75 px const height = this._props.rowHeight() ? this._props.rowHeight() - (this._props.padding || 6) * 2 : undefined; const width = height ? height * aspect : undefined; // increase the width of the image if necessary to maintain proportionality return ; } } @observer export class SchemaDateCell extends ObservableReactComponent { constructor(props: any) { super(props); makeObservable(this); } @observable _pickingDate: boolean = false; @computed get date(): DateField { // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined. return DateCast(this._props.Document[this._props.fieldKey]); } handleChange = undoable((date: Date | null) => { // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); // if (script.compiled) { // this.applyToDoc(this._document, this._props.row, this._props.col, script.run); // } else { // ^ DateCast is always undefined for some reason, but that is what the field should be set to date && (this._props.Document[this._props.fieldKey] = new DateField(date)); //} }, 'date change'); render() { const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); return ( <>
{}} />
{pointerEvents === 'none' ? null : ( } size={Size.XSMALL} type={Type.TERT} color={SettingsManager.userColor} background={SettingsManager.userBackgroundColor} popup={
} /> )} ); } } @observer export class SchemaRTFCell extends ObservableReactComponent { constructor(props: any) { super(props); makeObservable(this); } @computed get selected() { const selected = this._props.selectedCells(); return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col; //return this._props.isRowActive() && selected?.[0] === this._props.Document && selected[1] === this._props.col; } // if the text box blurs and none of its contents are focused(), then the edit finishes selectedFunc = () => this.selected; render() { const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); fieldProps.isContentActive = this.selectedFunc; return (
{this.selected ? this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
); } } @observer export class SchemaBoolCell extends ObservableReactComponent { constructor(props: any) { super(props); makeObservable(this); } @computed get selected() { const selected = this._props.selectedCells(); return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col; } render() { const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); return (
| undefined) => { if ((value?.nativeEvent as any).shiftKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); })} /> Field.toKeyValueString(this._props.Document, this._props.fieldKey)} SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value); this._props.finishEdit?.(); return true; } const set = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined); this._props.finishEdit?.(); return set; })} />
); } } @observer export class SchemaEnumerationCell extends ObservableReactComponent { constructor(props: any) { super(props); makeObservable(this); } @computed get selected() { const selected = this._props.selectedCells(); return this._props.isRowActive() && selected && selected?.filter(doc => doc === this._props.Document).length !== 0 && this._props.selectedCol() === this._props.col; } render() { const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); const options = this._props.options?.map(facet => ({ value: facet, label: facet })); return (