Skip to content

Commit

Permalink
feat: group new elements dropdown (#2513)
Browse files Browse the repository at this point in the history
  • Loading branch information
YannanGao-gs authored Aug 21, 2023
1 parent f07c3d2 commit c4adb7f
Show file tree
Hide file tree
Showing 17 changed files with 309 additions and 41 deletions.
8 changes: 8 additions & 0 deletions .changeset/happy-ears-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@finos/legend-extension-store-service-store': patch
'@finos/legend-extension-dsl-persistence': patch
'@finos/legend-extension-dsl-data-space': patch
'@finos/legend-extension-dsl-diagram': patch
'@finos/legend-extension-dsl-mastery': patch
'@finos/legend-extension-dsl-text': patch
---
5 changes: 5 additions & 0 deletions .changeset/sour-games-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@finos/legend-application-studio': minor
---

**BREAKING CHANGES** Group new element dropdown by category
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from '../../../stores/editor/NewElementState.js';
import { Dialog, compareLabelFn, CustomSelectorInput } from '@finos/legend-art';
import type { EditorStore } from '../../../stores/editor/EditorStore.js';
import { prettyCONSTName } from '@finos/legend-shared';
import { prettyCONSTName, toTitleCase } from '@finos/legend-shared';
import type { DSL_LegendStudioApplicationPlugin_Extension } from '../../../stores/LegendStudioApplicationPlugin.js';
import { useEditorStore } from '../EditorStoreProvider.js';
import {
Expand Down Expand Up @@ -552,6 +552,7 @@ export const CreateNewElementModal = observer(() => {
const applicationStore = useApplicationStore();
const newElementState = editorStore.newElementState;
const selectedPackage = newElementState.selectedPackage;
const elementLabel = getElementTypeLabel(editorStore, newElementState.type);
// Name
const name = newElementState.name;
const handleNameChange: React.ChangeEventHandler<HTMLInputElement> = (
Expand Down Expand Up @@ -615,8 +616,8 @@ export const CreateNewElementModal = observer(() => {
className="modal modal--dark search-modal"
>
<div className="modal__title">
{`Create a New ${
getElementTypeLabel(editorStore, newElementState.type) ?? 'element'
{`Create a new ${
elementLabel ? toTitleCase(elementLabel) : 'element'
}`}
</div>
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ import {
getPackageableElementOptionFormatter,
type PackageableElementOption,
} from '@finos/legend-lego/graph-editor';
import { PACKAGEABLE_ELEMENT_TYPE } from '../../../stores/editor/utils/ModelClassifierUtils.js';
import {
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY,
PACKAGEABLE_ELEMENT_TYPE,
} from '../../../stores/editor/utils/ModelClassifierUtils.js';
import { useLegendStudioApplicationStore } from '../../LegendStudioFrameworkProvider.js';
import { queryClass } from '../editor-group/uml-editor/ClassQueryBuilder.js';
import { createViewSDLCProjectHandler } from '../../../stores/editor/DependencyProjectViewerHelper.js';
Expand Down Expand Up @@ -498,14 +501,16 @@ const ExplorerContextMenu = observer(
? node.packageableElement
: undefined
: editorStore.graphManagerState.graph.root;
const elementTypes = ([PACKAGEABLE_ELEMENT_TYPE.PACKAGE] as string[])
.concat(editorStore.getSupportedElementTypes())
.filter(
// NOTE: we can only create package in root
(type) =>
_package !== editorStore.graphManagerState.graph.root ||
type === PACKAGEABLE_ELEMENT_TYPE.PACKAGE,
);

const elementTypesWithCategory =
_package === editorStore.graphManagerState.graph.root
? new Map<string, string[]>([
[
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.MODEL,
[PACKAGEABLE_ELEMENT_TYPE.PACKAGE],
],
])
: editorStore.supportedElementTypesWithCategory;

// actions
const buildQuery = editorStore.applicationStore.guardUnhandledError(
Expand Down Expand Up @@ -795,15 +800,27 @@ const ExplorerContextMenu = observer(
if (_package && !isReadOnly) {
return (
<MenuContent data-testid={LEGEND_STUDIO_TEST_ID.EXPLORER_CONTEXT_MENU}>
{elementTypes.map((type) => (
<MenuContentItem key={type} onClick={createNewElement(type)}>
<MenuContentItemIcon>
{getElementTypeIcon(type, editorStore)}
</MenuContentItemIcon>
<MenuContentItemLabel>
New {toTitleCase(getElementTypeLabel(editorStore, type))}...
</MenuContentItemLabel>
</MenuContentItem>
{Array.from(elementTypesWithCategory.entries()).map((entry) => (
<div
className="editor-group__view-mode__option__group editor-group__view-mode__option__group--native"
key={entry[0]}
>
<div className="editor-group__view-mode__option__group__name">
{entry[0]}
</div>
<div className="editor-group__view-mode__option__group__options editor-group__view-mode__option__group__options--center">
{entry[1].map((type) => (
<MenuContentItem key={type} onClick={createNewElement(type)}>
<MenuContentItemIcon>
{getElementTypeIcon(type, editorStore)}
</MenuContentItemIcon>
<MenuContentItemLabel>
{toTitleCase(getElementTypeLabel(editorStore, type))}
</MenuContentItemLabel>
</MenuContentItem>
))}
</div>
</div>
))}
{node && (
<>
Expand Down Expand Up @@ -1053,26 +1070,39 @@ const ExplorerDropdownMenu = observer(() => {
(): void =>
editorStore.newElementState.openModal(type, _package);

const elementTypes = ([PACKAGEABLE_ELEMENT_TYPE.PACKAGE] as string[])
.concat(editorStore.getSupportedElementTypes())
.filter(
// NOTE: we can only create package in root
(type) =>
_package !== editorStore.graphManagerState.graph.root ||
type === PACKAGEABLE_ELEMENT_TYPE.PACKAGE,
);
const elementTypesWithCategory =
_package === editorStore.graphManagerState.graph.root
? new Map<string, string[]>([
[
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.MODEL,
[PACKAGEABLE_ELEMENT_TYPE.PACKAGE],
],
])
: editorStore.supportedElementTypesWithCategory;

return (
<MenuContent data-testid={LEGEND_STUDIO_TEST_ID.EXPLORER_CONTEXT_MENU}>
{elementTypes.map((type) => (
<MenuContentItem key={type} onClick={createNewElement(type)}>
<MenuContentItemIcon>
{getElementTypeIcon(type, editorStore)}
</MenuContentItemIcon>
<MenuContentItemLabel>
New {toTitleCase(getElementTypeLabel(editorStore, type))}...
</MenuContentItemLabel>
</MenuContentItem>
{Array.from(elementTypesWithCategory.entries()).map((entry) => (
<div
className="editor-group__view-mode__option__group editor-group__view-mode__option__group--native"
key={entry[0]}
>
<div className="editor-group__view-mode__option__group__name">
{entry[0]}
</div>
<div className="editor-group__view-mode__option__group__options editor-group__view-mode__option__group__options--center">
{entry[1].map((type) => (
<MenuContentItem key={type} onClick={createNewElement(type)}>
<MenuContentItemIcon>
{getElementTypeIcon(type, editorStore)}
</MenuContentItemIcon>
<MenuContentItemLabel>
{toTitleCase(getElementTypeLabel(editorStore, type))}
</MenuContentItemLabel>
</MenuContentItem>
))}
</div>
</div>
))}
</MenuContent>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const addRootPackage = async (
): Promise<void> => {
fireEvent.click(result.getByTitle('New Element...', { exact: false }));
const contextMenu = await waitFor(() => result.getByRole('menu'));
fireEvent.click(getByText(contextMenu, 'New Package...'));
fireEvent.click(getByText(contextMenu, 'Package'));
const modal = result.getByTestId(LEGEND_STUDIO_TEST_ID.NEW_ELEMENT_MODAL);
const packageInput = getByPlaceholderText(modal, 'Enter a name', {
exact: false,
Expand All @@ -64,7 +64,7 @@ const createNewElementOnRootPackage = async (
fireEvent.contextMenu(pkgContainer);
const contextMenu = await waitFor(() => result.getByRole('menu'));
fireEvent.click(
getByText(contextMenu, `New ${toTitleCase(elementType.toLowerCase())}...`),
getByText(contextMenu, `${toTitleCase(elementType.toLowerCase())}`),
);
const modal = result.getByTestId(LEGEND_STUDIO_TEST_ID.NEW_ELEMENT_MODAL);
const elementInput = getByPlaceholderText(modal, 'Enter a name', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import type {
NewElementState,
} from '../../stores/editor/NewElementState.js';
import type { PureGrammarTextSuggestion } from '@finos/legend-lego/code-editor';
import { PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY } from '../../stores/editor/utils/ModelClassifierUtils.js';

const SCHEMA_SET_ELEMENT_TYPE = 'SCHEMASET';
const SCHEMA_SET_ELEMENT_PROJECT_EXPLORER_DND_TYPE =
Expand Down Expand Up @@ -148,6 +149,15 @@ export class DSL_ExternalFormat_LegendStudioApplicationPlugin
return [SCHEMA_SET_ELEMENT_TYPE, BINDING_ELEMENT_TYPE];
}

getExtraSupportedElementTypesWithCategory?(): Map<string, string[]> {
const elementTypesWithCategoryMap = new Map<string, string[]>();
elementTypesWithCategoryMap.set(
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.EXTERNAL_FORMAT,
[SCHEMA_SET_ELEMENT_TYPE, BINDING_ELEMENT_TYPE],
);
return elementTypesWithCategoryMap;
}

getExtraElementClassifiers(): ElementClassifier[] {
return [
(element: PackageableElement): string | undefined => {
Expand Down
1 change: 1 addition & 0 deletions packages/legend-application-studio/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export { generateServiceManagementUrl } from './stores/editor/editor-state/eleme
export { ServicePureExecutionState } from './stores/editor/editor-state/element-editor-state/service/ServiceExecutionState.js';
export { NewServiceModal } from './components/editor/editor-group/service-editor/NewServiceModal.js';
export { FileSystem_File as GenerationFile } from './stores/editor/utils/FileSystemTreeUtils.js';
export { PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY } from './stores/editor/utils/ModelClassifierUtils.js';
export {
FileGenerationState,
GeneratedFileStructureState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ export interface DSL_LegendStudioApplicationPlugin_Extension
*/
getExtraSupportedElementTypes?(): string[];

/**
* Get the Map of the supported packageable element type specifiers with its category.
*/
getExtraSupportedElementTypesWithCategory?(): Map<string, string[]>;

/**
* Get the list of classifiers for a packageable element.
*/
Expand Down
135 changes: 134 additions & 1 deletion packages/legend-application-studio/src/stores/editor/EditorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ import { LEGEND_STUDIO_APP_EVENT } from '../../__lib__/LegendStudioEvent.js';
import type { EditorMode } from './EditorMode.js';
import { StandardEditorMode } from './StandardEditorMode.js';
import { WorkspaceUpdateConflictResolutionState } from './sidebar-state/WorkspaceUpdateConflictResolutionState.js';
import { PACKAGEABLE_ELEMENT_TYPE } from './utils/ModelClassifierUtils.js';
import {
PACKAGEABLE_ELEMENT_TYPE,
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY,
} from './utils/ModelClassifierUtils.js';
import { GlobalTestRunnerState } from './sidebar-state/testable/GlobalTestRunnerState.js';
import type { LegendStudioApplicationStore } from '../LegendStudioBaseStore.js';
import { EmbeddedQueryBuilderState } from './EmbeddedQueryBuilderState.js';
Expand Down Expand Up @@ -188,6 +191,7 @@ export class EditorStore implements CommandRegistrar {
snap: 150,
});
readonly tabManagerState = new EditorTabManagerState(this);
supportedElementTypesWithCategory: Map<string, string[]>;

constructor(
applicationStore: LegendStudioApplicationStore,
Expand Down Expand Up @@ -285,6 +289,8 @@ export class EditorStore implements CommandRegistrar {
)
.map((creator) => creator(this))
.filter(isNonNullable);
this.supportedElementTypesWithCategory =
this.getSupportedElementTypesWithCategory();
}

get isInitialized(): boolean {
Expand Down Expand Up @@ -926,6 +932,133 @@ export class EditorStore implements CommandRegistrar {
}
}

getSupportedElementTypesWithCategory(): Map<string, string[]> {
const elementTypesWithCategoryMap = new Map<string, string[]>();
Object.values(PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY).forEach((value) => {
switch (value) {
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.MODEL: {
const elements = [
PACKAGEABLE_ELEMENT_TYPE.PACKAGE,
PACKAGEABLE_ELEMENT_TYPE.CLASS,
PACKAGEABLE_ELEMENT_TYPE.ASSOCIATION,
PACKAGEABLE_ELEMENT_TYPE.ENUMERATION,
PACKAGEABLE_ELEMENT_TYPE.PROFILE,
PACKAGEABLE_ELEMENT_TYPE.FUNCTION,
PACKAGEABLE_ELEMENT_TYPE.MEASURE,
PACKAGEABLE_ELEMENT_TYPE.DATA,
] as string[];
elementTypesWithCategoryMap.set(
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.MODEL,
elements,
);
break;
}
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.STORE: {
const elements = [
PACKAGEABLE_ELEMENT_TYPE.DATABASE,
PACKAGEABLE_ELEMENT_TYPE.FLAT_DATA_STORE,
] as string[];
elementTypesWithCategoryMap.set(
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.STORE,
elements,
);
break;
}
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.QUERY: {
const elements = [
PACKAGEABLE_ELEMENT_TYPE.CONNECTION,
PACKAGEABLE_ELEMENT_TYPE.RUNTIME,
PACKAGEABLE_ELEMENT_TYPE.MAPPING,
PACKAGEABLE_ELEMENT_TYPE.SERVICE,
this.applicationStore.config.options
.TEMPORARY__enableLocalConnectionBuilder
? PACKAGEABLE_ELEMENT_TYPE.TEMPORARY__LOCAL_CONNECTION
: undefined,
] as (string | undefined)[];
elementTypesWithCategoryMap.set(
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.QUERY,
elements.filter(isNonNullable),
);
break;
}
// for displaying categories in order
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.EXTERNAL_FORMAT: {
elementTypesWithCategoryMap.set(
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.EXTERNAL_FORMAT,
[],
);
break;
}
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.GENERATION: {
const elements = [
PACKAGEABLE_ELEMENT_TYPE.FILE_GENERATION,
PACKAGEABLE_ELEMENT_TYPE.GENERATION_SPECIFICATION,
] as string[];
elementTypesWithCategoryMap.set(
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.GENERATION,
elements,
);
break;
}
// for displaying categories in order
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.OTHER: {
elementTypesWithCategoryMap.set(
PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.OTHER,
[],
);
break;
}
default:
break;
}
});
const extensions = this.pluginManager
.getApplicationPlugins()
.flatMap(
(plugin) =>
(
plugin as DSL_LegendStudioApplicationPlugin_Extension
).getExtraSupportedElementTypesWithCategory?.() ??
new Map<string, string[]>(),
);
const elementTypesWithCategoryMapFromExtensions = new Map<
string,
string[]
>();
extensions.forEach((typeCategoryMap) => {
Array.from(typeCategoryMap.entries()).forEach((entry) => {
const [key, value] = entry;
elementTypesWithCategoryMapFromExtensions.set(
key,
elementTypesWithCategoryMapFromExtensions.get(key) === undefined
? [...value]
: [
...guaranteeNonNullable(
elementTypesWithCategoryMapFromExtensions.get(key),
),
...value,
],
);
});
});
// sort extensions alphabetically and insert extensions into the base elementTypesWithCategoryMap
Array.from(elementTypesWithCategoryMapFromExtensions.entries()).forEach(
(entry) => {
const [key, value] = entry;
value.sort((a, b) => a.localeCompare(b));
const existingValues = elementTypesWithCategoryMap.get(key);
elementTypesWithCategoryMap.set(
key,
existingValues === undefined
? [...value]
: [...guaranteeNonNullable(existingValues), ...value],
);
},
);

return elementTypesWithCategoryMap;
}

getSupportedElementTypes(): string[] {
return (
[
Expand Down
Loading

0 comments on commit c4adb7f

Please sign in to comment.