import { ColumnRec, DocModel, ViewSectionRec } from 'app/client/models/DocModel'; import { IPageWidget } from 'app/client/ui/PageWidgetPicker'; import { removePrefix } from 'app/common/gutil'; import { IOptionFull } from 'grainjs'; // some unicode characters const BLACK_CIRCLE = '\u2022'; const RIGHT_ARROW = '\u2192'; // Describes a link export interface IPageWidgetLink { // The source section id srcSectionRef: number; // The source column id srcColRef: number; // The target col id targetColRef: number; } export const NoLink = linkId({ srcSectionRef: 0, srcColRef: 0, targetColRef: 0 }); const NoLinkOption: IOptionFull = { label: "Select Widget", value: NoLink }; interface LinkNode { // the tableId tableId: string; // is the table a summary table isSummary: boolean; // list of ids of the sections that are ancestors to this section according to the linked section // relationship ancestors: Set; // the section record. Must be the empty record sections that are to be created. section: ViewSectionRec; // the column record or undefined for the main section node (ie: the node that does not connect to // any particular column) column?: ColumnRec; // the widget type widgetType: string; } // Returns true is the link from `source` to `target` is valid, false otherwise. function isValidLink(source: LinkNode, target: LinkNode) { // section must not be the same if (source.section.getRowId() === target.section.getRowId()) { return false; } // table must match if (source.tableId !== target.tableId) { return false; } // summary table can only link to and from the main node (node with no column) if ((source.isSummary || target.isSummary) && (source.column || target.column)) { return false; } // cannot select from chart or custom if (['chart', 'custom'].includes(source.widgetType)) { return false; } // The link must not create a cycle if (source.ancestors.has(target.section.getRowId())) { return false; } return true; } // Represents the differents way to reference to a section for linking type MaybeSection = ViewSectionRec|IPageWidget; // Returns a list of options with all links that link one of the `source` section to the `target` // section. Each `opt.value` is a unique identifier (see: linkId() and linkFromId() for more // detail), and `opt.label` is a human readable representation of the form // `[.][ -> ]` where the appears // only when linking from a reference column, as opposed to linking from the table directly. And the // shows only when both [.] is ambiguous. export function selectBy(docModel: DocModel, sources: ViewSectionRec[], target: MaybeSection): Array> { const sourceNodes = createNodes(docModel, sources); const targetNodes = createNodes(docModel, [target]); const options = [NoLinkOption]; for (const srcNode of sourceNodes) { const validTargets = targetNodes.filter((tgt) => isValidLink(srcNode, tgt)); const hasMany = validTargets.length > 1; for (const tgtNode of validTargets) { // a unique identifier for this link const value = linkId({ srcSectionRef: srcNode.section.getRowId(), srcColRef: srcNode.column ? srcNode.column.getRowId() : 0, targetColRef: tgtNode.column ? tgtNode.column.getRowId() : 0, }); // a human readable description let label = srcNode.section.titleDef(); // add the source node col name or nothing for table node label += srcNode.column ? ` ${BLACK_CIRCLE} ${srcNode.column.label.peek()}` : ''; // add the target column name only if target has multiple valid nodes label += hasMany && tgtNode.column ? ` ${RIGHT_ARROW} ${tgtNode.column.label.peek()}` : ''; // add the new option options.push({ label, value }); } } return options; } function isViewSectionRec(section: MaybeSection): section is ViewSectionRec { return Boolean((section as ViewSectionRec).getRowId); } // Create all nodes for sections. function createNodes(docModel: DocModel, sections: MaybeSection[]) { const nodes = []; for (const section of sections) { if (isViewSectionRec(section)) { nodes.push(...fromViewSectionRec(section)); } else { nodes.push(...fromPageWidget(docModel, section)); } } return nodes; } // Creates an array of LinkNode from a view section record. function fromViewSectionRec(section: ViewSectionRec): LinkNode[] { const table = section.table.peek(); const columns = table.columns.peek().peek(); const ancestors = new Set(); for (let sec = section; sec.getRowId(); sec = sec.linkSrcSection.peek()) { if (ancestors.has(sec.getRowId())) { // tslint:disable-next-line:no-console console.warn(`Links should not create a cycle - section ids: ${Array.from(ancestors)}`); break; } ancestors.add(sec.getRowId()); } const mainNode = { tableId: table.primaryTableId.peek(), isSummary: table.primaryTableId.peek() !== table.tableId.peek(), widgetType: section.parentKey.peek(), ancestors, section, }; const nodes: LinkNode[] = [mainNode]; // add the column nodes for (const column of columns) { const tableId = removePrefix(column.type.peek(), 'Ref:'); if (tableId) { nodes.push({...mainNode, tableId, column}); } } return nodes; } // Creates an array of LinkNode from a page widget. function fromPageWidget(docModel: DocModel, pageWidget: IPageWidget): LinkNode[] { if (typeof pageWidget.table !== 'number') { return []; } const table = docModel.tables.getRowModel(pageWidget.table); const columns = table.columns.peek().peek(); const mainNode: LinkNode = { tableId: table.primaryTableId.peek(), isSummary: pageWidget.summarize, widgetType: pageWidget.type, ancestors: new Set(), section: docModel.viewSections.getRowModel(pageWidget.section), }; const nodes: LinkNode[] = [mainNode]; // adds the column nodes for (const column of columns) { const tableId = removePrefix(column.type.peek(), 'Ref:'); if (tableId) { nodes.push({...mainNode, tableId, column}); } } return nodes; } // Returns an identifier to uniquely identify a link. Here we adopt a simple approach where // {srcSectionRef: 2, srcColRef: 3, targetColRef: 3} is turned into "[2, 3, 3]". export function linkId(link: IPageWidgetLink) { return JSON.stringify([link.srcSectionRef, link.srcColRef, link.targetColRef]); } // Returns link's properties from its identifier. export function linkFromId(linkid: string) { const [srcSectionRef, srcColRef, targetColRef] = JSON.parse(linkid); return {srcSectionRef, srcColRef, targetColRef}; }