/**
* Copyright (c) 2020-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 { useApplicationStore } from '@finos/legend-application';
import { observer } from 'mobx-react-lite';
import {
BlankPanelContent,
Panel,
PanelLoadingIndicator,
TreeView,
type TreeNodeContainerProps,
ChevronDownIcon,
ChevronRightIcon,
GenericTextFileIcon,
FolderIcon,
FolderOpenIcon,
HomeIcon,
SearchIcon,
TimesIcon,
CodeIcon,
clsx,
CopyIcon,
compareLabelFn,
} from '@finos/legend-art';
import {
SHOWCASE_MANAGER_SEARCH_CATEGORY,
SHOWCASE_MANAGER_VIEW,
ShowcaseManagerState,
type ShowcaseTextSearchMatchResult,
type ShowcasesExplorerTreeNodeData,
} from '../stores/ShowcaseManagerState.js';
import {
SHOWCASE_LAUNCH_ENTRY_POINT,
SHOWCASE_FEEDBACK_SURFACE,
} from '../__lib__/LegendStudioTelemetryHelper.js';
import { ShowcaseFeedback } from './showcase/ShowcaseFeedback.js';
import { debounce, isNonNullable } from '@finos/legend-shared';
import { flowResult } from 'mobx';
import type { Showcase } from '@finos/legend-server-showcase';
import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor';
import { CodeEditor } from '@finos/legend-lego/code-editor';
import React, { useEffect, useMemo, useRef } from 'react';
import { generateShowcasePath } from '../__lib__/LegendStudioNavigation.js';
const ShowcasesExplorerTreeNodeContainer = observer(
(
props: TreeNodeContainerProps<
ShowcasesExplorerTreeNodeData,
{
showcaseManagerState: ShowcaseManagerState;
toggleExpandNode: (node: ShowcasesExplorerTreeNodeData) => void;
}
>,
) => {
const { node, level, innerProps } = props;
const { toggleExpandNode, showcaseManagerState } = innerProps;
const applicationStore = useApplicationStore();
const expandIcon = !node.metadata ? (
node.isOpen ? (
) : (
)
) : (
);
const nodeIcon = !node.metadata ? (
node.isOpen ? (
) : (
)
) : (
);
const onNodeClick = (): void => {
if (!node.metadata) {
toggleExpandNode(node);
} else {
flowResult(
showcaseManagerState.openShowcase(
node.metadata,
undefined,
SHOWCASE_LAUNCH_ENTRY_POINT.EXPLORER,
),
).catch(applicationStore.alertUnhandledError);
}
};
return (
{expandIcon}
{nodeIcon}
{node.label}
);
},
);
const ShowcaseManagerExplorer = observer(
(props: { showcaseManagerState: ShowcaseManagerState }) => {
const { showcaseManagerState } = props;
const treeData = showcaseManagerState.explorerTreeData;
const getChildNodes = (
node: ShowcasesExplorerTreeNodeData,
): ShowcasesExplorerTreeNodeData[] => {
if (treeData) {
return node.childrenIds
.map((id) => treeData.nodes.get(id))
.filter(isNonNullable)
.sort(compareLabelFn);
}
return [];
};
const toggleExpandNode = (node: ShowcasesExplorerTreeNodeData): void => {
if (treeData) {
node.isOpen = !node.isOpen;
showcaseManagerState.setExplorerTreeData({ ...treeData });
}
};
return (
{
showcaseManagerState.closeShowcase();
showcaseManagerState.setCurrentView(SHOWCASE_MANAGER_VIEW.SEARCH);
}}
>
{showcaseManagerState.explorerTreeData && (
)}
);
},
);
const renderPreviewLine = (
line: number,
text: string,
result: ShowcaseTextSearchMatchResult,
): React.ReactNode => {
const lineMatches = result.match.matches
.filter((match) => match.line === line)
.sort((a, b) => a.startColumn - b.startColumn);
const chunks: React.ReactNode[] = [];
let currentIdx = 0;
lineMatches.forEach((match, idx) => {
if (currentIdx < match.startColumn - 1) {
chunks.push(text.substring(currentIdx, match.startColumn - 1));
}
chunks.push(
{text.substring(match.startColumn - 1, match.endColumn - 1)}
,
);
currentIdx = match.endColumn - 1;
});
if (currentIdx < text.length) {
chunks.push(text.substring(currentIdx, text.length));
}
return (
<>
{chunks.map((chunk, idx) => (
// eslint-disable-next-line react/no-array-index-key
{chunk}
))}
>
);
};
const ShowcaseManagerCodeSearchResult = observer(
(props: {
showcaseManagerState: ShowcaseManagerState;
result: ShowcaseTextSearchMatchResult;
}) => {
const { showcaseManagerState, result } = props;
const applicationStore = useApplicationStore();
return (
{
flowResult(
showcaseManagerState.openShowcase(
result.showcase,
undefined,
SHOWCASE_LAUNCH_ENTRY_POINT.SEARCH_SHOWCASE_MATCH,
),
).catch(applicationStore.alertUnhandledError);
}}
>
{result.showcase.path}
{result.match.preview.map((entry) => (
{
flowResult(
showcaseManagerState.openShowcase(
result.showcase,
entry.line,
SHOWCASE_LAUNCH_ENTRY_POINT.SEARCH_CODE_MATCH,
),
).catch(applicationStore.alertUnhandledError);
}}
>
match.line === entry.line,
),
),
},
)}
>
{entry.line}
{renderPreviewLine(entry.line, entry.text, result)}
))}
);
},
);
const ShowcaseManagerSearchPanel = observer(
(props: { showcaseManagerState: ShowcaseManagerState }) => {
const { showcaseManagerState } = props;
const applicationStore = useApplicationStore();
const searchTextInpurRef = useRef(null);
const debouncedSearch = useMemo(
() =>
debounce(
() =>
flowResult(showcaseManagerState.search()).catch(
applicationStore.alertUnhandledError,
),
300,
),
[applicationStore, showcaseManagerState],
);
const clearSearchText = (): void => {
debouncedSearch.cancel();
showcaseManagerState.resetSearch();
};
const onSearchTextChange: React.ChangeEventHandler = (
event,
): void => {
const value = event.target.value;
showcaseManagerState.setSearchText(value);
debouncedSearch.cancel();
if (!value) {
showcaseManagerState.resetSearch();
} else {
debouncedSearch()?.catch(applicationStore.alertUnhandledError);
}
};
const onSearchKeyDown: React.KeyboardEventHandler = (
event,
) => {
if (event.code === 'Enter') {
debouncedSearch.cancel();
if (showcaseManagerState.searchText) {
debouncedSearch()?.catch(applicationStore.alertUnhandledError);
}
} else if (event.code === 'Escape') {
searchTextInpurRef.current?.select();
}
};
useEffect(() => {
searchTextInpurRef.current?.select();
searchTextInpurRef.current?.focus();
}, []);
return (
{
showcaseManagerState.closeShowcase();
showcaseManagerState.setCurrentView(
SHOWCASE_MANAGER_VIEW.EXPLORER,
);
}}
>
Showcases
{
// since we're already on the search tab, clicking this will just focus the search input
searchTextInpurRef.current?.select();
}}
>
{!showcaseManagerState.searchText ? (
) : (
)}
showcaseManagerState.setCurrentSearchCategory(
SHOWCASE_MANAGER_SEARCH_CATEGORY.SHOWCASE,
)
}
title="Click to select category"
>
{showcaseManagerState.showcaseSearchResults?.length ?? 0}
showcaseManagerState.setCurrentSearchCategory(
SHOWCASE_MANAGER_SEARCH_CATEGORY.CODE,
)
}
title="Click to select category"
>
{showcaseManagerState.textSearchResults?.length ?? 0}
{showcaseManagerState.currentSearchCaterogy ===
SHOWCASE_MANAGER_SEARCH_CATEGORY.SHOWCASE && (
<>
{!showcaseManagerState.showcaseSearchResults?.length && (
No results
)}
{showcaseManagerState.showcaseSearchResults?.map(
(showcase) => (
{
flowResult(
showcaseManagerState.openShowcase(showcase),
).catch(applicationStore.alertUnhandledError);
}}
>
{showcase.title}
{showcase.description ?? '(no description)'}
),
)}
>
)}
{showcaseManagerState.currentSearchCaterogy ===
SHOWCASE_MANAGER_SEARCH_CATEGORY.CODE && (
<>
{!showcaseManagerState.textSearchResults?.length && (
No results
)}
{showcaseManagerState.textSearchResults?.map((result) => (
))}
>
)}
);
},
);
const ShowcaseViewer = observer(
(props: {
showcaseManagerState: ShowcaseManagerState;
showcase: Showcase;
}) => {
const { showcaseManagerState, showcase } = props;
const prettyPath = showcase.path.replaceAll(/\s*\/\s*/g, ' / ');
const launchShowcase = (): void => {
const applicationStore = showcaseManagerState.applicationStore;
applicationStore.navigationService.navigator.visitAddress(
applicationStore.navigationService.navigator.generateAddress(
generateShowcasePath(showcase.path),
),
);
};
const handleCopy =
showcaseManagerState.applicationStore.guardUnhandledError(() =>
showcaseManagerState.applicationStore.clipboardService.copyTextToClipboard(
showcase.code,
{
notifySuccessMessage: 'Showcase grammar copied to clipboard',
},
),
);
return (
{
showcaseManagerState.closeShowcase();
showcaseManagerState.setCurrentView(
SHOWCASE_MANAGER_VIEW.EXPLORER,
);
}}
>
Showcases
{
showcaseManagerState.closeShowcase();
showcaseManagerState.setCurrentView(SHOWCASE_MANAGER_VIEW.SEARCH);
}}
>
);
},
);
const ShowcaseManagerContent = observer(
(props: { showcaseManagerState: ShowcaseManagerState }) => {
const { showcaseManagerState } = props;
const currentShowcase = showcaseManagerState.currentShowcase;
const currentView = showcaseManagerState.currentView;
return (
{currentShowcase && (
)}
{!currentShowcase && (
<>
{currentView === SHOWCASE_MANAGER_VIEW.EXPLORER && (
)}
{currentView === SHOWCASE_MANAGER_VIEW.SEARCH && (
)}
>
)}
);
},
);
export const ShowcaseManager = observer(() => {
const applicationStore = useApplicationStore();
const showcaseManagerState =
ShowcaseManagerState.retrieveNullableState(applicationStore);
if (!showcaseManagerState) {
return null;
}
return (
{showcaseManagerState.initState.isInProgress && (
Initializing...
)}
{showcaseManagerState.initState.hasFailed && (
Failed to initialize
)}
{showcaseManagerState.initState.hasSucceeded && (
)}
);
});