UNPKG

@finos/legend-lego

Version:
411 lines 17 kB
/** * Copyright (c) 2025-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {} from '@finos/legend-art'; import { CORE_PURE_PATH, ELEMENT_PATH_DELIMITER } from '@finos/legend-graph'; import { ActionState, filterByType, FuzzySearchAdvancedConfigState, FuzzySearchEngine, guaranteeNonNullable, } from '@finos/legend-shared'; import { action, computed, makeObservable, observable } from 'mobx'; import { AssociationDocumentationEntry, ClassDocumentationEntry, EnumerationDocumentationEntry, ModelDocumentationEntry, } from './ModelDocumentationAnalysis.js'; export var ModelsDocumentationFilterTreeNodeCheckType; (function (ModelsDocumentationFilterTreeNodeCheckType) { ModelsDocumentationFilterTreeNodeCheckType[ModelsDocumentationFilterTreeNodeCheckType["CHECKED"] = 0] = "CHECKED"; ModelsDocumentationFilterTreeNodeCheckType[ModelsDocumentationFilterTreeNodeCheckType["UNCHECKED"] = 1] = "UNCHECKED"; ModelsDocumentationFilterTreeNodeCheckType[ModelsDocumentationFilterTreeNodeCheckType["PARTIALLY_CHECKED"] = 2] = "PARTIALLY_CHECKED"; })(ModelsDocumentationFilterTreeNodeCheckType || (ModelsDocumentationFilterTreeNodeCheckType = {})); export class ModelsDocumentationFilterTreeNodeData { id; label; parentNode; isOpen = false; childrenIds = []; childrenNodes = []; // By default all nodes are checked checkType = ModelsDocumentationFilterTreeNodeCheckType.CHECKED; constructor(id, label, parentNode) { makeObservable(this, { isOpen: observable, checkType: observable, setIsOpen: action, setCheckType: action, }); this.id = id; this.label = label; this.parentNode = parentNode; } setIsOpen(val) { this.isOpen = val; } setCheckType(val) { this.checkType = val; } } export class ModelsDocumentationFilterTreeRootNodeData extends ModelsDocumentationFilterTreeNodeData { } export class ModelsDocumentationFilterTreePackageNodeData extends ModelsDocumentationFilterTreeNodeData { packagePath; constructor(id, label, parentNode, packagePath) { super(id, label, parentNode); this.packagePath = packagePath; } } export class ModelsDocumentationFilterTreeElementNodeData extends ModelsDocumentationFilterTreeNodeData { elementPath; typePath; constructor(id, label, parentNode, elementPath, typePath) { super(id, label, parentNode); this.elementPath = elementPath; this.typePath = typePath; } } export class ModelsDocumentationFilterTreeTypeNodeData extends ModelsDocumentationFilterTreeNodeData { typePath; constructor(id, label, parentNode, typePath) { super(id, label, parentNode); this.typePath = typePath; } } export const trickleDownUncheckNodeChildren = (node) => { node.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.UNCHECKED); node.childrenNodes.forEach((childNode) => trickleDownUncheckNodeChildren(childNode)); }; export const trickleUpUncheckNode = (node) => { const parentNode = node.parentNode; if (!parentNode) { return; } if (parentNode.childrenNodes.some((childNode) => childNode.checkType === ModelsDocumentationFilterTreeNodeCheckType.CHECKED)) { parentNode.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.PARTIALLY_CHECKED); } else { parentNode.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.UNCHECKED); } trickleUpUncheckNode(parentNode); }; export const uncheckFilterTreeNode = (node) => { trickleDownUncheckNodeChildren(node); trickleUpUncheckNode(node); }; export const trickleDownCheckNode = (node) => { node.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.CHECKED); node.childrenNodes.forEach((childNode) => trickleDownCheckNode(childNode)); }; export const trickleUpCheckNode = (node) => { const parentNode = node.parentNode; if (!parentNode) { return; } if (parentNode.childrenNodes.every((childNode) => childNode.checkType === ModelsDocumentationFilterTreeNodeCheckType.CHECKED)) { parentNode.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.CHECKED); } else { parentNode.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.PARTIALLY_CHECKED); } trickleUpCheckNode(parentNode); }; export const checkFilterTreeNode = (node) => { trickleDownCheckNode(node); trickleUpCheckNode(node); }; export const uncheckAllFilterTree = (treeData) => { treeData.nodes.forEach((node) => node.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.UNCHECKED)); }; export const buildTypeFilterTreeData = () => { const rootIds = []; const nodes = new Map(); // all node const allNode = new ModelsDocumentationFilterTreeRootNodeData('all', 'All Types', undefined); rootIds.push(allNode.id); allNode.setIsOpen(true); // open the root node by default nodes.set(allNode.id, allNode); // type nodes const classNode = new ModelsDocumentationFilterTreeTypeNodeData('class', 'Class', allNode, CORE_PURE_PATH.CLASS); allNode.childrenIds.push(classNode.id); nodes.set(classNode.id, classNode); const enumerationNode = new ModelsDocumentationFilterTreeTypeNodeData('enumeration', 'Enumeration', allNode, CORE_PURE_PATH.ENUMERATION); allNode.childrenIds.push(enumerationNode.id); nodes.set(enumerationNode.id, enumerationNode); const associationNode = new ModelsDocumentationFilterTreeTypeNodeData('association', 'Association', allNode, CORE_PURE_PATH.ASSOCIATION); allNode.childrenIds.push(associationNode.id); nodes.set(associationNode.id, associationNode); allNode.childrenNodes = [classNode, enumerationNode, associationNode]; return { rootIds, nodes, }; }; export const buildPackageFilterTreeData = (modelDocEntries) => { const rootIds = []; const nodes = new Map(); // all node const allNode = new ModelsDocumentationFilterTreeRootNodeData('all', 'All Packages', undefined); rootIds.push(allNode.id); allNode.setIsOpen(true); // open the root node by default nodes.set(allNode.id, allNode); modelDocEntries.forEach((entry) => { const path = entry.path; const chunks = path.split(ELEMENT_PATH_DELIMITER); let currentParentNode = allNode; for (let i = 0; i < chunks.length; i++) { const chunk = guaranteeNonNullable(chunks[i]); const elementPath = `${currentParentNode === allNode ? '' : `${currentParentNode.id}${ELEMENT_PATH_DELIMITER}`}${chunk}`; const nodeId = elementPath; let node = nodes.get(nodeId); if (!node) { if (i === chunks.length - 1) { node = new ModelsDocumentationFilterTreeElementNodeData(nodeId, chunk, currentParentNode, elementPath, entry instanceof ClassDocumentationEntry ? CORE_PURE_PATH.CLASS : entry instanceof EnumerationDocumentationEntry ? CORE_PURE_PATH.ENUMERATION : entry instanceof AssociationDocumentationEntry ? CORE_PURE_PATH.ASSOCIATION : undefined); } else { node = new ModelsDocumentationFilterTreePackageNodeData(nodeId, chunk, currentParentNode, elementPath); } nodes.set(nodeId, node); currentParentNode.childrenIds.push(nodeId); currentParentNode.childrenNodes.push(node); } currentParentNode = node; } }); return { rootIds, nodes, }; }; export class ViewerModelsDocumentationState { showHumanizedForm = true; searchInput; searchEngine; searchConfigurationState; searchState = ActionState.create(); elementDocs; searchText; searchResults = []; showSearchConfigurationMenu = false; packageFilterTreeData; constructor(elementDocs) { makeObservable(this, { showHumanizedForm: observable, searchText: observable, // NOTE: we use `observable.struct` for these to avoid unnecessary re-rendering of the grid searchResults: observable.struct, filterTypes: observable.struct, filterPaths: observable.struct, showSearchConfigurationMenu: observable, showFilterPanel: observable, typeFilterTreeData: observable.ref, packageFilterTreeData: observable.ref, filteredSearchResults: computed, isTypeFilterCustomized: computed, isPackageFilterCustomized: computed, isFilterCustomized: computed, setShowHumanizedForm: action, setSearchText: action, resetSearch: action, search: action, setShowSearchConfigurationMenu: action, setShowFilterPanel: action, resetPackageFilterTreeData: action, resetTypeFilterTreeData: action, updateTypeFilter: action, updatePackageFilter: action, resetTypeFilter: action, resetPackageFilter: action, resetAllFilters: action, }); this.searchConfigurationState = new FuzzySearchAdvancedConfigState(() => this.search()); this.elementDocs = elementDocs; this.searchText = ''; this.typeFilterTreeData = buildTypeFilterTreeData(); this.updateTypeFilter(); this.packageFilterTreeData = buildPackageFilterTreeData(this.elementDocs .map((entry) => entry.entry) .filter(filterByType(ModelDocumentationEntry))); this.searchResults = elementDocs; this.updatePackageFilter(); this.searchEngine = new FuzzySearchEngine(elementDocs, { includeScore: true, // NOTE: we must not sort/change the order in the grid since // we want to ensure the element row is on top shouldSort: false, // Ignore location when computing the search score // See https://fusejs.io/concepts/scoring-theory.html ignoreLocation: true, // This specifies the point the search gives up // `0.0` means exact match where `1.0` would match anything // We set a relatively low threshold to filter out irrelevant results threshold: 0.2, keys: [ { name: 'text', weight: 3, }, { name: 'humanizedText', weight: 3, }, { name: 'elementEntry.name', weight: 3, }, { name: 'elementEntry.humanizedName', weight: 3, }, { name: 'entry.name', weight: 2, }, { name: 'entry.humanizedName', weight: 2, }, { name: 'documentation', weight: 4, }, ], // extended search allows for exact word match through single quote // See https://fusejs.io/examples.html#extended-search useExtendedSearch: true, }); } get isFilterCustomized() { return this.isTypeFilterCustomized || this.isPackageFilterCustomized; } get isPackageFilterCustomized() { return Array.from(this.packageFilterTreeData.nodes.values()).some((node) => node.checkType === ModelsDocumentationFilterTreeNodeCheckType.UNCHECKED); } get filteredSearchResults() { return this.searchResults .filter((result) => (this.filterTypes.includes(CORE_PURE_PATH.CLASS) && result.elementEntry instanceof ClassDocumentationEntry) || (this.filterTypes.includes(CORE_PURE_PATH.ENUMERATION) && result.elementEntry instanceof EnumerationDocumentationEntry) || (this.filterTypes.includes(CORE_PURE_PATH.ASSOCIATION) && result.elementEntry instanceof AssociationDocumentationEntry)) .filter((result) => this.filterPaths.includes(result.elementEntry.path)); } get isTypeFilterCustomized() { return Array.from(this.typeFilterTreeData.nodes.values()).some((node) => node.checkType === ModelsDocumentationFilterTreeNodeCheckType.UNCHECKED); } resetAllFilters() { this.resetTypeFilter(); this.resetPackageFilter(); } resetSearch() { this.searchText = ''; this.searchResults = this.elementDocs; this.searchState.complete(); } search() { if (!this.searchText) { this.searchResults = this.elementDocs; return; } this.searchState.inProgress(); this.searchResults = this.performSearch(this.searchConfigurationState.generateSearchText(this.searchText)); this.searchState.complete(); } showFilterPanel = true; typeFilterTreeData; filterTypes = []; filterPaths = []; resetPackageFilterTreeData() { this.packageFilterTreeData = { ...this.packageFilterTreeData }; } hasClassDocumentation(classPath) { return this.elementDocs.some((entry) => entry.elementEntry.path === classPath); } viewClassDocumentation(classPath) { if (this.hasClassDocumentation(classPath)) { const classNode = this.packageFilterTreeData.nodes.get(classPath); if (classNode) { uncheckAllFilterTree(this.packageFilterTreeData); trickleDownCheckNode(classNode); trickleUpCheckNode(classNode); classNode.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.CHECKED); this.resetSearch(); this.updatePackageFilter(); } } } updatePackageFilter() { const elementPaths = []; this.packageFilterTreeData.nodes.forEach((node) => { if (node instanceof ModelsDocumentationFilterTreeElementNodeData && node.checkType === ModelsDocumentationFilterTreeNodeCheckType.CHECKED) { elementPaths.push(node.elementPath); } }); this.filterPaths = elementPaths.toSorted((a, b) => a.localeCompare(b)); } resetPackageFilter() { this.packageFilterTreeData.nodes.forEach((node) => node.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.CHECKED)); this.updatePackageFilter(); this.resetPackageFilterTreeData(); } performSearch(searchText) { return Array.from(this.searchEngine.search(searchText).values()).map((result) => result.item); } setShowHumanizedForm(val) { this.showHumanizedForm = val; } setSearchText(val) { this.searchText = val; } setShowSearchConfigurationMenu(val) { this.showSearchConfigurationMenu = val; } setShowFilterPanel(val) { this.showFilterPanel = val; } resetTypeFilterTreeData() { this.typeFilterTreeData = { ...this.typeFilterTreeData }; } updateTypeFilter() { const types = []; this.typeFilterTreeData.nodes.forEach((node) => { if (node instanceof ModelsDocumentationFilterTreeTypeNodeData && node.checkType === ModelsDocumentationFilterTreeNodeCheckType.CHECKED) { types.push(node.typePath); } }); // NOTE: sort to avoid unnecessary re-computation of filtered search results this.filterTypes = types.toSorted((a, b) => a.localeCompare(b)); } resetTypeFilter() { this.typeFilterTreeData.nodes.forEach((node) => node.setCheckType(ModelsDocumentationFilterTreeNodeCheckType.CHECKED)); this.updateTypeFilter(); this.resetTypeFilterTreeData(); } setSearchInput(el) { this.searchInput = el; } focusSearchInput() { this.searchInput?.focus(); } selectSearchInput() { this.searchInput?.select(); } } //# sourceMappingURL=ModelDocumentationState.js.map