aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-06-14 09:12:13 -0400
committerbobzel <zzzman@gmail.com>2023-06-14 09:12:13 -0400
commit376270791c7fe414c05a87f73afe11146d119c35 (patch)
treec6c788c958a5aaca4a9bbdd709d5e6f1d76dde0d /src/client/views/collections/collectionSchema/SchemaTableCell.tsx
parent2bc89733ce522527c2f27203b537d99395c9479b (diff)
parentbf16eca7a84adfdf1c5970e7e4793568ee70325d (diff)
Merge branch 'master' into advanced-trails
Diffstat (limited to 'src/client/views/collections/collectionSchema/SchemaTableCell.tsx')
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx337
1 files changed, 311 insertions, 26 deletions
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 5f8ffe8b0..97264508c 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -1,64 +1,349 @@
-import React = require('react');
+import * as React from 'react';
+import Select, { MenuPlacement } from 'react-select';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, Field } from '../../../../fields/Doc';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils';
+import { extname } from 'path';
+import DatePicker from 'react-datepicker';
+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 { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero, Utils } from '../../../../Utils';
+import { FInfo } from '../../../documents/Documents';
+import { DocFocusOrOpen } from '../../../util/DocumentManager';
import { Transform } from '../../../util/Transform';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { EditableView } from '../../EditableView';
+import { Colors } from '../../global/globalEnums';
+import { OpenWhere } from '../../nodes/DocumentView';
import { FieldView, FieldViewProps } from '../../nodes/FieldView';
+import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
import { KeyValueBox } from '../../nodes/KeyValueBox';
import { DefaultStyleProvider } from '../../StyleProvider';
-import { CollectionSchemaView } from './CollectionSchemaView';
+import { CollectionSchemaView, ColumnType, FInfotoColType } from './CollectionSchemaView';
import './CollectionSchemaView.scss';
export interface SchemaTableCellProps {
Document: Doc;
+ col: number;
+ deselectCell: () => void;
+ selectCell: (doc: Doc, col: number) => void;
+ selectedCell: () => [Doc, number] | undefined;
fieldKey: string;
- columnWidth: number;
+ 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;
+ 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;
}
@observer
export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
- render() {
- const props: FieldViewProps = {
- Document: this.props.Document,
- docFilters: returnEmptyFilter,
- docRangeFilters: returnEmptyFilter,
+ 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';
+ const textDecoration = color !== 'black' && parenCount ? 'underline' : '';
+ const fieldProps: FieldViewProps = {
+ childFilters: returnEmptyFilter,
+ childFiltersByRanges: returnEmptyFilter,
searchFilterDocs: returnEmptyDoclist,
styleProvider: DefaultStyleProvider,
docViewPath: returnEmptyDoclist,
- fieldKey: this.props.fieldKey,
rootSelected: returnFalse,
isSelected: returnFalse,
setHeight: returnFalse,
select: emptyFunction,
- dropAction: 'alias',
+ dropAction: 'embed',
bringToFront: emptyFunction,
renderDepth: 1,
isContentActive: returnFalse,
whenChildContentsActiveChanged: emptyFunction,
ScreenToLocalTransform: Transform.Identity,
focus: emptyFunction,
- PanelWidth: () => this.props.columnWidth,
- PanelHeight: () => CollectionSchemaView._rowHeight,
- addDocTab: returnFalse,
+ addDocTab: SchemaTableCell.addFieldDoc,
pinToPres: returnZero,
+ Document,
+ fieldKey: fieldKey,
+ PanelWidth: columnWidth,
+ PanelHeight: props.rowHeight,
};
+ 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 selected: [Doc, number] | undefined = this.props.selectedCell();
+ return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col;
+ }
+
+ @computed get defaultCellContent() {
+ const { color, textDecoration, fieldProps } = SchemaTableCell.renderProps(this.props);
+
+ return (
+ <div
+ className="schemacell-edit-wrapper"
+ style={{
+ color,
+ textDecoration,
+ width: '100%',
+ }}>
+ <EditableView
+ oneLine={this.props.oneLine}
+ allowCRs={this.props.allowCRs}
+ contents={<FieldView {...fieldProps} />}
+ editing={this.selected ? undefined : false}
+ GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)}
+ SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
+ if (shiftDown && enterKey) {
+ this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value);
+ }
+ const ret = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value);
+ this.props.finishEdit?.();
+ return ret;
+ }, 'edit schema cell')}
+ />
+ </div>
+ );
+ }
+
+ 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 !== '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 <SchemaImageCell {...this.props} />;
+ case ColumnType.Boolean: return <SchemaBoolCell {...this.props} />;
+ case ColumnType.RTF: return <SchemaRTFCell {...this.props} />;
+ case ColumnType.Enumeration: return <SchemaEnumerationCell {...this.props} options={this.props.getFinfo(this.props.fieldKey)?.values?.map(val => val.toString())} />;
+ case ColumnType.Date: // return <SchemaDateCell {...this.props} />;
+ default: return this.defaultCellContent;
+ }
+ }
+
+ render() {
+ return (
+ <div
+ className="schema-table-cell"
+ onPointerDown={action(e => !this.selected && this.props.selectCell(this.props.Document, this.props.col))}
+ 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}
+ </div>
+ );
+ }
+}
+
+// mj: most of this is adapted from old schema code so I'm not sure what it does tbh
+@observer
+export class SchemaImageCell extends React.Component<SchemaTableCellProps> {
+ @observable _previewRef: HTMLImageElement | 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 <img src={this.url} width={width} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />;
+ }
+}
+
+@observer
+export class SchemaDateCell extends React.Component<SchemaTableCellProps> {
+ @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]);
+ }
+
+ @action
+ handleChange = (date: any) => {
+ // 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
+ this.props.Document[this.props.fieldKey] = new DateField(date as Date);
+ //}
+ };
+
+ render() {
+ return <DatePicker dateFormat={'Pp'} selected={this.date.date} onChange={(date: any) => this.handleChange(date)} />;
+ }
+}
+@observer
+export class SchemaRTFCell extends React.Component<SchemaTableCellProps> {
+ @computed get selected() {
+ const selected: [Doc, number] | undefined = this.props.selectedCell();
+ return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col;
+ }
+ selectedFunc = () => this.selected;
+ render() {
+ const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props);
+ fieldProps.isContentActive = this.selectedFunc;
+ return (
+ <div className="schemaRTFCell" style={{ display: 'flex', fontStyle: this.selected ? undefined : 'italic', width: '100%', height: '100%', position: 'relative', color, textDecoration, cursor, pointerEvents }}>
+ {this.selected ? <FormattedTextBox allowScroll={true} {...fieldProps} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
+ </div>
+ );
+ }
+}
+@observer
+export class SchemaBoolCell extends React.Component<SchemaTableCellProps> {
+ @computed get selected() {
+ const selected: [Doc, number] | undefined = this.props.selectedCell();
+ return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col;
+ }
+ render() {
+ const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props);
+ return (
+ <div className="schemaBoolCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}>
+ <input
+ style={{ marginRight: 4 }}
+ type="checkbox"
+ checked={BoolCast(this.props.Document[this.props.fieldKey])}
+ onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => {
+ if ((value?.nativeEvent as any).shiftKey) {
+ this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+ }
+ KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+ })}
+ />
+ <EditableView
+ contents={<FieldView {...fieldProps} />}
+ editing={this.selected ? undefined : false}
+ GetValue={() => 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);
+ }
+ const set = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value);
+ this.props.finishEdit?.();
+ return set;
+ })}
+ />
+ </div>
+ );
+ }
+}
+@observer
+export class SchemaEnumerationCell extends React.Component<SchemaTableCellProps> {
+ @computed get selected() {
+ const selected: [Doc, number] | undefined = this.props.selectedCell();
+ return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === 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 (
- <div className="schema-table-cell" style={{ width: this.props.columnWidth }}>
- <div className="schemacell-edit-wrapper" style={this.props.isRowActive() ? { cursor: 'text', pointerEvents: 'auto' } : { cursor: 'default', pointerEvents: 'none' }}>
- <EditableView
- contents={<FieldView {...props} />}
- GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)}
- SetValue={(value: string, shiftDown?: boolean, enterKey?: boolean) => {
- if (shiftDown && enterKey) {
- this.props.setColumnValues(this.props.fieldKey, value);
- }
- return KeyValueBox.SetField(this.props.Document, this.props.fieldKey, value);
+ <div className="schemaSelectionCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}>
+ <div style={{ width: '100%' }}>
+ <Select
+ styles={{
+ menuPortal: base => ({
+ ...base,
+ left: 0,
+ top: 0,
+ transform: `translate(${this.props.transform().TranslateX}px, ${this.props.transform().TranslateY}px)`,
+ width: Number(base.width) * this.props.transform().Scale,
+ zIndex: 9999,
+ }),
}}
- editing={this.props.isRowActive() ? undefined : false}
+ menuPortalTarget={this.props.menuTarget}
+ menuPosition={'absolute'}
+ placeholder={StrCast(this.props.Document[this.props.fieldKey], 'select...')}
+ options={options}
+ isMulti={false}
+ onChange={val => KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)}
/>
</div>
</div>