@finos/legend-lego
Version:
Legend code editor support
411 lines • 17 kB
JavaScript
/**
* 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