diff options
Diffstat (limited to 'src')
3 files changed, 114 insertions, 200 deletions
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 9623b0d12..0e5a1e6b6 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -47,6 +47,18 @@ import { threadId } from 'worker_threads'; import { FontIconBox } from '../../nodes/FontIconBox/FontIconBox'; import { SnappingManager } from '../../../util/SnappingManager'; +/** + * The schema view offers a spreadsheet-like interface for users to interact with documents. Within the schema, + * each doc is represented by its own row. Each column represents a field, for example the author or title fields. + * Users can apply varoius filters and sorts to columns to change what is displayed. The schemaview supports equations for + * cell linking. + * + * This class supports the main functionality for choosing which docs to render in the view, applying visual + * updates to rows and columns (such as user dragging or sort-related highlighting), applying edits to multiple cells + * at once, and applying filters and sorts to columns. It contains SchemaRowBoxes (which themselves contain SchemaTableCells, + * and SchemaCellFields) and SchemaColumnHeaders. + */ + const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export const FInfotoColType: { [key: string]: ColumnType } = { @@ -109,19 +121,19 @@ export class CollectionSchemaView extends CollectionSubView() { @observable _newFieldType: ColumnType = ColumnType.Number; @observable _menuValue: string = ''; @observable _filterColumnIndex: number | undefined = undefined; - @observable _filterSearchValue: string = ''; + @observable _filterSearchValue: string = ''; //the current text inside the filter search bar, used to determine which values to display @observable _selectedCol: number = 0; @observable _selectedCells: Array<Doc> = []; @observable _mouseCoordinates = { x: 0, y: 0, prevX: 0, prevY: 0 }; @observable _lowestSelectedIndex: number = -1; //lowest index among selected rows; used to properly sync dragged docs with cursor position @observable _relCursorIndex: number = -1; //cursor index relative to the current selected cells @observable _draggedColIndex: number = 0; - @observable _colBeingDragged: boolean = false; + @observable _colBeingDragged: boolean = false; //whether a column is being dragged by the user @observable _colKeysFiltered: boolean = false; @observable _cellTags: ObservableMap = new ObservableMap<Doc, Array<string>>(); @observable _highlightedCellsInfo: Array<[doc: Doc, field: string]> = []; @observable _cellHighlightColors: ObservableMap = new ObservableMap<string, string[]>(); - @observable _docs: Doc[] = []; + @observable _containedDocs: Doc[] = []; //all direct children of the schema @observable _referenceSelectMode: {enabled: boolean, currEditing: SchemaCellField | undefined} = {enabled: false, currEditing: undefined} // target HTMLelement portal for showing a popup menu to edit cell values. @@ -218,11 +230,11 @@ export class CollectionSchemaView extends CollectionSubView() { ); this._disposers.docdata = reaction( () => DocListCast(this.dataDoc[this.fieldKey]), - (docs) => this._docs = docs, + (docs) => this._containedDocs = docs, {fireImmediately: true} ) this._disposers.sortHighlight = reaction( - () => [this.sortField, this._docs, this._selectedDocs, this._highlightedCellsInfo], + () => [this.sortField, this._containedDocs, this._selectedDocs, this._highlightedCellsInfo], () => {this.sortField && setTimeout(() => this.highlightSortedColumn())}, {fireImmediately: true} ) @@ -239,7 +251,7 @@ export class CollectionSchemaView extends CollectionSubView() { removeDoc = (doc: Doc) => { this.removeDocument(doc); - this._docs = this._docs.filter(d => d !== doc) + this._containedDocs = this._containedDocs.filter(d => d !== doc) } rowIndex = (doc: Doc) => this.docsWithDrag.docs.indexOf(doc); @@ -301,11 +313,7 @@ export class CollectionSchemaView extends CollectionSubView() { } break; case 'Backspace': { - // this._docs.forEach(doc => { - // if (!this.childDocs.concat(this.displayedSubCollectionDocs(this.Document))) - // }); - // console.log('backspace detected') - undoable(() => {this._selectedDocs.forEach(d => this._docs.includes(d) && this.removeDoc(d));}, 'delete schema row'); + undoable(() => {this._selectedDocs.forEach(d => this._containedDocs.includes(d) && this.removeDoc(d));}, 'delete schema row'); break; } case 'Escape': { @@ -320,9 +328,6 @@ export class CollectionSchemaView extends CollectionSubView() { } }; - @action - changeSelectedCellColumn = () => {}; - addRow = (doc: Doc | Doc[]) => this.addDocument(doc); @undoBatch @@ -363,7 +368,7 @@ export class CollectionSchemaView extends CollectionSubView() { if (this.columnKeys.length === 1) return; if (this._columnMenuIndex === index) { this._headerRefs[index].toggleEditing(false); - this.closeColumnMenu(); + this.closeNewColumnMenu(); } const currWidths = this.storedColumnWidths.slice(); currWidths.splice(index, 1); @@ -436,7 +441,7 @@ export class CollectionSchemaView extends CollectionSubView() { @action dragColumn = (e: PointerEvent, index: number) => { - this.closeColumnMenu(); + this.closeNewColumnMenu(); this._headerRefs.forEach(ref => ref.toggleEditing(false)); this._draggedColIndex = index; this.setColDrag(true); @@ -447,6 +452,11 @@ export class CollectionSchemaView extends CollectionSubView() { return true; }; + /** + * Uses cursor x coordinate to calculate which index the column should be rendered/dropped in + * @param mouseX cursor x coordinate + * @returns column index + */ findColDropIndex = (mouseX: number) => { let xOffset: number = this._props.ScreenToLocalTransform().inverse().transformPoint(0,0)[0] + CollectionSchemaView._rowMenuWidth; let index: number | undefined; @@ -461,32 +471,11 @@ export class CollectionSchemaView extends CollectionSubView() { }; /** - * Calculates the relative index of the cursor in the group of selected rows, ie. - * if five rows are selected and the cursor is in the middle row, its relative index would be 2. - * Used to align actively dragged documents properly with the cursor. - * @param mouseY the initial Y position of the cursor on drag + * Calculates the current index of dragged rows for dynamic rendering and drop logic. + * @param mouseY user's cursor position relative to the viewport + * @returns row index the dragged doc should be rendered/dropped in */ - @action - setRelCursorIndex = (mouseY: number) => { - this._mouseCoordinates.y = mouseY; // updates this.rowDropIndex computed value to overwrite the old cached value - - const rowHeight = CollectionSchemaView._rowHeight; - const adjInitMouseY = mouseY - rowHeight - 100; // rowHeight: height of the column menu cells | 100: height of the top menu - const yOffset = this._lowestSelectedIndex * rowHeight; - - const heights = this._selectedDocs.map(() => this.rowHeightFunc()); - let index: number = 0; - heights.reduce((total, curr, i) => { - if (total <= adjInitMouseY && total + curr >= adjInitMouseY) { - if (adjInitMouseY <= total + curr) index = i; - else index = i + 1; - } - return total + curr; - }, yOffset); - this._relCursorIndex = index; - }; - - findRowDropIndex = (mouseY: number) => { + findRowDropIndex = (mouseY: number): number => { const rowHeight = CollectionSchemaView._rowHeight; let index: number = 0; this.rowHeights.reduce((total, curr, i) => { @@ -545,10 +534,16 @@ export class CollectionSchemaView extends CollectionSubView() { }); } + /** + * Applies a gradient highlight to a sorted column. The direction of the gradient depends + * on whether the sort is ascending or descending. + * @param field the column being sorted + * @param descending whether the sort is descending or ascending; descending if true + */ highlightSortedColumn = (field?: string, descending?: boolean) => { let index = -1; let highlightColors: string[] = []; - const rowCount: number = this._docs.length + 1; + const rowCount: number = this._containedDocs.length + 1; if (field || this.sortField){ index = this.columnKeys.indexOf(field || this.sortField); const increment: number = 110/rowCount; @@ -581,12 +576,23 @@ export class CollectionSchemaView extends CollectionSubView() { } + /** + * Gets the html element representing a cell so that styles can be applied on it. + * @param doc the cell's row document + * @param fieldKey the cell's column's field key + * @returns the html element representing the cell at the given location + */ getCellElement = (doc: Doc, fieldKey: string) => { const index = this.columnKeys.indexOf(fieldKey); const cell = this._rowEles.get(doc).children[1].children[index]; return cell; } + /** + * Given text in a cell, find references to other cells (for equations). + * @param text the text in the cell + * @returns the html cell elements referenced in the text. + */ findCellRefs = (text: string) => { const pattern = /(this|d(\d+))\.(\w+)/g; interface Match { docRef: string; field: string; } @@ -604,12 +610,18 @@ export class CollectionSchemaView extends CollectionSubView() { const {docRef, field} = match; const docView = DocumentManager.Instance.DocumentViews[Number(docRef)]; const doc = docView?.Document ?? undefined; - if (this.columnKeys.includes(field) && this._docs.includes(doc)) {cells.push([doc, field])} + if (this.columnKeys.includes(field) && this._containedDocs.includes(doc)) {cells.push([doc, field])} }) return cells; } + /** + * Determines whether the rows above or below a given row have been + * selected, so selection highlights don't overlap. + * @param doc the document row to check + * @returns a boolean tuple where 0 is the row above, and 1 is the row below + */ selectionOverlap = (doc: Doc): [boolean, boolean] => { const docs = this.docsWithDrag.docs; const index = this.rowIndex(doc); @@ -647,6 +659,11 @@ export class CollectionSchemaView extends CollectionSubView() { }); } + /** + * Highlights cells based on equation text in the cell currently being edited. + * Does not highlight selected cells (that's done directly in SchemaTableCell). + * @param text the equation + */ highlightCells = (text: string) => { this.removeCellHighlights(); @@ -660,7 +677,6 @@ export class CollectionSchemaView extends CollectionSubView() { const doc = info[0]; const field = info[1]; const key = `${doc[Id]}_${field}`; - console.log(key + ' ' + i % 10 + ' color: ' + color[0].r + color[0].g + color[0].b); const cell = this.getCellElement(doc, field); this._cellHighlightColors.set(key, [colorStrings[0], colorStrings[1]]); cell.style.border = colorStrings[0]; @@ -668,6 +684,7 @@ export class CollectionSchemaView extends CollectionSubView() { } } + //Used in SchemaRowBox @action addRowRef = (doc: Doc, ref: HTMLDivElement) => this._rowEles.set(doc, ref); @@ -693,7 +710,8 @@ export class CollectionSchemaView extends CollectionSubView() { this.deselectAllCells(); }; - selectRows = (doc: Doc, lastSelected: Doc) => { + + selectRow = (doc: Doc, lastSelected: Doc) => { const index = this.rowIndex(doc); const lastSelectedRow = this.rowIndex(lastSelected); const startRow = Math.min(lastSelectedRow, index); @@ -706,6 +724,7 @@ export class CollectionSchemaView extends CollectionSubView() { } }; + //Used in SchemaRowBox selectReference = (doc: Doc | undefined, col: number) => { if (!doc) return; const docIndex = DocumentView.getDocViewIndex(doc); @@ -719,7 +738,7 @@ export class CollectionSchemaView extends CollectionSubView() { @action selectCell = (doc: Doc, col: number, shiftKey: boolean, ctrlKey: boolean) => { - this.closeColumnMenu(); + this.closeNewColumnMenu(); if (!shiftKey && !ctrlKey) this.clearSelection(); !this._selectedCells && (this._selectedCells = []); !shiftKey && this._selectedCells.push(doc); @@ -727,7 +746,7 @@ export class CollectionSchemaView extends CollectionSubView() { if (!this) return; const lastSelected = Array.from(this._selectedDocs).lastElement(); - if (shiftKey && lastSelected && !this._selectedDocs.includes(doc)) this.selectRows(doc, lastSelected); + if (shiftKey && lastSelected && !this._selectedDocs.includes(doc)) this.selectRow(doc, lastSelected); else if (ctrlKey) { if (lastSelected && this._selectedDocs.includes(doc)) { DocumentView.DeselectView(DocumentView.getFirstDocumentView(doc)); @@ -821,71 +840,6 @@ export class CollectionSchemaView extends CollectionSubView() { return undefined; }; - @computed get fieldDefaultInput() { - switch (this._newFieldType) { - case ColumnType.Number: - return ( - <input - type="number" - name="" - id="" - value={this._newFieldDefault ?? 0} - onPointerDown={e => e.stopPropagation()} - onChange={action((e: any) => { - this._newFieldDefault = e.target.value; - })} - /> - ); - case ColumnType.Boolean: - return ( - <> - <input - type="checkbox" - name="" - id="" - value={this._newFieldDefault} - onPointerDown={e => e.stopPropagation()} - onChange={action((e: any) => { - this._newFieldDefault = e.target.checked; - })} - /> - {this._newFieldDefault ? 'true' : 'false'} - </> - ); - case ColumnType.String: - return ( - <input - type="text" - name="" - id="" - value={this._newFieldDefault ?? ''} - onPointerDown={e => e.stopPropagation()} - onChange={action((e: any) => { - this._newFieldDefault = e.target.value; - })} - /> - ); - default: - return undefined; - } - } - - onSearchKeyDown = (e: React.KeyboardEvent) => { - switch (e.key) { - case 'Enter': - this._menuKeys.length > 0 && this._menuValue.length > 0 - ? this.setKey(this._menuKeys[0]) - : runInAction(() => { - this._makeNewField = true; - }); - break; - case 'Escape': - this.closeColumnMenu(); - break; - default: - } - }; - @action setKey = (key: string, defaultVal?: any, index?: number) => { if (this.columnKeys.includes(key)) return; @@ -895,9 +849,15 @@ export class CollectionSchemaView extends CollectionSubView() { this._makeNewColumn = false; } else this.changeColumnKey(this._columnMenuIndex! | index!, key, defaultVal); - this.closeColumnMenu(); + this.closeNewColumnMenu(); }; + /** + * Used in SchemaRowBox to set + * @param key + * @param value + * @returns + */ setCellValues = (key: string, value: string) => { if (this._selectedCells.length === 1) this.docs.forEach(doc => !doc._lockedSchemaEditing && Doc.SetField(doc, key, value)); else this._selectedCells.forEach(doc => !doc._lockedSchemaEditing && Doc.SetField(doc, key, value)); @@ -905,24 +865,7 @@ export class CollectionSchemaView extends CollectionSubView() { }; @action - toggleMenuKeyFilter = () => { - if (!this._colKeysFiltered){ - this._colKeysFiltered = true; - this._menuKeys = this.documentKeys.filter(key => this.childDocsInclude(key)); - } else { - this._colKeysFiltered = false; - this._menuKeys = this.documentKeys; - } - } - - childDocsInclude = (key: string) => { - let keyExists: boolean = false; - this.childDocs.forEach(doc => {if (Object.keys(doc).includes(key)) keyExists = true;}) - return keyExists - } - - @action - openColumnMenu = (index: number, newCol: boolean) => { + openNewColumnMenu = (index: number, newCol: boolean) => { this.closeFilterMenu(); this._makeNewColumn = false; @@ -934,7 +877,7 @@ export class CollectionSchemaView extends CollectionSubView() { }; @action - closeColumnMenu = () => { + closeNewColumnMenu = () => { this._columnMenuIndex = undefined; }; @@ -956,7 +899,7 @@ export class CollectionSchemaView extends CollectionSubView() { }; openContextMenu = (x: number, y: number, index: number) => { - this.closeColumnMenu(); + this.closeNewColumnMenu(); this.closeFilterMenu(); const cm = ContextMenu.Instance; cm.clearItems(); @@ -970,7 +913,7 @@ export class CollectionSchemaView extends CollectionSubView() { event: () => { this.setColumnSort(undefined); const field = this.columnKeys[index]; - this._docs = this.sortDocs(field, false); + this._containedDocs = this.sortDocs(field, false); setTimeout(() => { this.highlightSortedColumn(field, false); setTimeout(() => this.highlightSortedColumn(), 480); @@ -982,7 +925,7 @@ export class CollectionSchemaView extends CollectionSubView() { event: () => { this.setColumnSort(undefined); const field = this.columnKeys[index]; - this._docs = this.sortDocs(field, true); + this._containedDocs = this.sortDocs(field, true); setTimeout(() => { this.highlightSortedColumn(field, true); setTimeout(() => this.highlightSortedColumn(), 480); @@ -1016,7 +959,7 @@ export class CollectionSchemaView extends CollectionSubView() { cm.addItem({ description: `Change field`, - event: () => this.openColumnMenu(index, false), + event: () => this.openNewColumnMenu(index, false), icon: 'pencil-alt', }); cm.addItem({ @@ -1049,6 +992,7 @@ export class CollectionSchemaView extends CollectionSubView() { cm.displayMenu(x, y, undefined, false); }; + //used in schemacolumnheader @action updateKeySearch = (val: string) => { this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(val.toLowerCase())); @@ -1075,70 +1019,12 @@ export class CollectionSchemaView extends CollectionSubView() { this._filterSearchValue = e.target.value; }; - // @computed get newFieldMenu() { - // return ( - // <div className="schema-new-key-options"> - // <div className="schema-key-type-option"> - // <input - // type="radio" - // name="newFieldType" - // checked={this._newFieldType === ColumnType.Number} - // onChange={action(() => { - // this._newFieldType = ColumnType.Number; - // this._newFieldDefault = 0; - // })} - // /> - // number - // </div> - // <div className="schema-key-type-option"> - // <input - // type="radio" - // name="newFieldType" - // checked={this._newFieldType === ColumnType.Boolean} - // onChange={action(() => { - // this._newFieldType = ColumnType.Boolean; - // this._newFieldDefault = false; - // })} - // /> - // boolean - // </div> - // <div className="schema-key-type-option"> - // <input - // type="radio" - // name="newFieldType" - // checked={this._newFieldType === ColumnType.String} - // onChange={action(() => { - // this._newFieldType = ColumnType.String; - // this._newFieldDefault = ''; - // })} - // /> - // string - // </div> - // <div className="schema-key-default-val">value: {this.fieldDefaultInput}</div> - // <div className="schema-key-warning">{this._newFieldWarning}</div> - // <div - // className="schema-column-menu-button" - // onPointerDown={action(() => { - // if (this.documentKeys.includes(this._menuValue)) { - // this._newFieldWarning = 'Field already exists'; - // } else if (this._menuValue.length === 0) { - // this._newFieldWarning = 'Field cannot be an empty string'; - // } else { - // this.setKey(this._menuValue, this._newFieldDefault); - // } - // this._columnMenuIndex = undefined; - // })}> - // done - // </div> - // </div> - // ); - // } - onKeysPassiveWheel = (e: WheelEvent) => { // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) if (!this._oldKeysWheel.scrollTop && e.deltaY <= 0) e.preventDefault(); e.stopPropagation(); }; + _oldKeysWheel: any; @computed get keysDropdown() { return ( @@ -1262,6 +1148,12 @@ export class CollectionSchemaView extends CollectionSubView() { } }; + /** + * Gets docs contained by collections within the schema + * @param doc + * @param displayed + * @returns + */ subCollectionDocs = (doc: Doc, displayed: boolean) => { const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]); let collections: Array<Doc> = []; @@ -1272,13 +1164,16 @@ export class CollectionSchemaView extends CollectionSubView() { return toReturn; } + /** + * Applies any filters active on the schema to filter out docs that don't match. + */ @computed get filteredDocs() { const childDocFilters = this.childDocFilters(); const childFiltersByRanges = this.childDocRangeFilters(); const searchDocs = this.searchFilterDocs(); const docsforFilter: Doc[] = []; - this._docs.forEach(d => { + this._containedDocs.forEach(d => { // dragging facets const dragged = this._props.childFilters?.().some(f => f.includes(ClientUtils.noDragDocsFilter)); if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return; @@ -1315,12 +1210,15 @@ export class CollectionSchemaView extends CollectionSubView() { return docsforFilter; } + /** + * Returns all child docs of the schema and child docs of contained collections that satisfy applied filters. + */ @computed get docs() { let docsFromChildren: Doc[] = []; const displayedCollections = this.childDocs.filter(d => d.type === 'collection' && d._childrenSharedWithSchema); displayedCollections.forEach(d => { - let docsNotAlreadyDisplayed = this.subCollectionDocs(d, true).filter(dc => !this._docs.includes(dc)); + let docsNotAlreadyDisplayed = this.subCollectionDocs(d, true).filter(dc => !this._containedDocs.includes(dc)); docsFromChildren = docsFromChildren.concat(docsNotAlreadyDisplayed); }); let docs = this.filteredDocs; @@ -1328,6 +1226,13 @@ export class CollectionSchemaView extends CollectionSubView() { return docs; } + /** + * Sorts docs first alphabetically and then numerically. + * @param field the column being sorted + * @param desc whether the sort is ascending or descending + * @param persistent whether the sort is applied persistently or is one-shot + * @returns + */ sortDocs = (field: string, desc: boolean, persistent?: boolean) => { const numbers: Doc[] = []; const strings: Doc[] = []; @@ -1349,10 +1254,13 @@ export class CollectionSchemaView extends CollectionSubView() { } else sortedStrings = strings.slice().sort((docB, docA) => collator.compare(Field.toString(docA[field] as FieldType), Field.toString(docB[field] as FieldType))); const sortedDocs = desc ? sortedNums.concat(sortedStrings) : sortedStrings.concat(sortedNums); - if (!persistent) this._docs = sortedDocs; + if (!persistent) this._containedDocs = sortedDocs; return sortedDocs; } + /** + * Returns all docs minus those currently being dragged by the user. + */ @computed get docsWithDrag() { let docs = this.docs.slice(); if (this.sortField){ @@ -1380,7 +1288,7 @@ export class CollectionSchemaView extends CollectionSubView() { <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)} onPointerMove={e => this.onPointerMove(e)} - onPointerDown={() => {this.closeColumnMenu(); this.setColDrag(false)}}> + onPointerDown={() => {this.closeNewColumnMenu(); this.setColDrag(false)}}> <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }} /> <div className="schema-table" diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx index 9f6478041..6da236d82 100644 --- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -71,7 +71,7 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea setColumnValues = (field: string, defaultValue: string) => {this._props.schemaView?.setKey(field, defaultValue, this._props.columnIndex);} @action updateAlt = (newAlt: string) => {this._altTitle = newAlt}; updateKeyDropdown = (value: string) => {this._props.schemaView.updateKeySearch(value)}; - openKeyDropdown = () => {!this._props.schemaView._colBeingDragged && this._props.schemaView.openColumnMenu(this._props.columnIndex, false)}; + openKeyDropdown = () => {!this._props.schemaView._colBeingDragged && this._props.schemaView.openNewColumnMenu(this._props.columnIndex, false)}; toggleEditing = (editing: boolean) => { this._inputRef?.setIsEditing(editing); this._inputRef?.setIsFocused(editing); diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx index 8afd1bbf1..bb9ea7e17 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx @@ -631,6 +631,12 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { // return new Doc; // } + /** + * Populates a preset template framework with content from a datavizbox or any AI-generated content. + * @param template the preloaded template framework being filled in + * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz + * @returns a doc containing the fully rendered template + */ fillPresetTemplate = async(template: TemplateDocInfos, assignments: {[field: string]: Col}): Promise<Doc> => { const wordLimit = (size: TemplateFieldSize) => { switch (size) { |