feat: Updated new tab ui with search and load more (#34981)

## Description

Updated new tab ui with search, load more and updated titles.


Fixes #34809, #34530

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/10036807357>
> Commit: 2d37c01455f48de14510fa17cae104dd7578091c
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=10036807357&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Mon, 22 Jul 2024 08:46:24 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Added search functionality in the JS and Query Editor Panes to filter
and display grouped items based on search terms.
- Introduced a new component to render grouped lists with dynamic load
more functionality.

- **Enhancements**
- Improved text and terminology for clearer user understanding in
various modules.
- Enhanced layout alignment by adjusting component heights to account
for editor tabs.

- **Refactor**
- Improved type safety and refined logic in multiple functions for
better performance and maintainability.
- Replaced hardcoded values with constants for consistent and easier
management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
albinAppsmith 2024-07-23 10:56:04 +05:30 committed by GitHub
parent a38ef9a096
commit 5127005129
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 238 additions and 97 deletions

View File

@ -2312,10 +2312,17 @@ export const EDITOR_PANE_TEXTS = {
query_create_tab_title: () => "Create new query from",
widgets_create_tab_title: () => "Drag & drop UI elements",
js_create_tab_title: () => "Create JS object from",
queries_create_from_existing: () => "From existing datasource",
queries_create_new: () => "New API",
js_create_modules: () => "JS modules (Beta)",
queries_create_from_existing: () => "Datasources",
queries_create_new: () => "Quick actions",
queries_create_modules: () => "Query modules (Beta)",
loading_building_blocks: () => "Loading building blocks",
empty_search_result: (type: string) => `No ${type} match your search`,
search_objects: {
jsObject: () => "JS object",
queries: () => "queries",
datasources: () => "datasources",
},
};
export const PARTIAL_IMPORT_EXPORT = {

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from "react";
import { useCallback, useMemo } from "react";
import history from "utils/history";
import { useLocation } from "react-router";
import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity";
@ -33,7 +33,7 @@ import ListQuery from "pages/Editor/IDE/EditorPane/Query/List";
import type { AppState } from "@appsmith/reducers";
import keyBy from "lodash/keyBy";
import { getPluginEntityIcon } from "pages/Editor/Explorer/ExplorerIcons";
import { Tag, type ListItemProps } from "design-system";
import type { ListItemProps } from "design-system";
import { useCurrentEditorState } from "pages/Editor/IDE/hooks";
import { createAddClassName } from "pages/Editor/IDE/EditorPane/utils";
import { QueriesBlankState } from "pages/Editor/QueryEditor/QueriesBlankState";
@ -182,12 +182,16 @@ export const useAddQueryListItems = () => {
[pageId, dispatch],
);
const getListItems = (data: any[]) => {
const getListItems = (data: ActionOperation[]) => {
return data.map((fileOperation) => {
const title =
let title =
fileOperation.entityExplorerTitle ||
fileOperation.dsName ||
fileOperation.title;
title =
fileOperation.focusEntityType === FocusEntity.QUERY_MODULE_INSTANCE
? fileOperation.title
: title;
const className = createAddClassName(title);
const icon =
fileOperation.icon ||
@ -197,11 +201,10 @@ export const useAddQueryListItems = () => {
startIcon: icon,
wrapperClassName: className,
title,
description: !!fileOperation.isBeta ? (
<Tag isClosable={false}>Beta</Tag>
) : (
""
),
description:
fileOperation.focusEntityType === FocusEntity.QUERY_MODULE_INSTANCE
? fileOperation.dsName
: "",
descriptionType: "inline",
onClick: onCreateItemClick.bind(null, fileOperation),
} as ListItemProps;

View File

@ -1,8 +1,8 @@
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import SegmentAddHeader from "../components/SegmentAddHeader";
import { EDITOR_PANE_TEXTS } from "@appsmith/constants/messages";
import { EDITOR_PANE_TEXTS, createMessage } from "@appsmith/constants/messages";
import type { ListItemProps } from "design-system";
import { Flex, Tag } from "design-system";
import { Flex, SearchInput } from "design-system";
import { useDispatch, useSelector } from "react-redux";
import { getCurrentPageId } from "selectors/editorSelectors";
import GroupedList from "../components/GroupedList";
@ -12,11 +12,15 @@ import {
} from "@appsmith/pages/Editor/IDE/EditorPane/JS/hooks";
import type { ActionOperation } from "components/editorComponents/GlobalSearch/utils";
import type { AddProps } from "../types/AddProps";
import { createAddClassName } from "../utils";
import { createAddClassName, fuzzySearchInObjectItems } from "../utils";
import { FocusEntity } from "navigation/FocusEntity";
import type { GroupedListProps } from "../components/types";
import { EmptySearchResult } from "../components/EmptySearchResult";
const AddJS = ({ containerProps, innerContainerProps }: AddProps) => {
const dispatch = useDispatch();
const pageId = useSelector(getCurrentPageId);
const [searchTerm, setSearchTerm] = useState("");
const groupedJsOperations = useGroupedAddJsOperations();
@ -35,13 +39,29 @@ const AddJS = ({ containerProps, innerContainerProps }: AddProps) => {
return {
startIcon: data.icon,
title,
description: !!data.isBeta ? <Tag isClosable={false}>Beta</Tag> : "",
description:
data.focusEntityType === FocusEntity.JS_MODULE_INSTANCE
? data.dsName
: "",
descriptionType: "inline",
onClick: onCreateItemClick.bind(null, data),
wrapperClassName: createAddClassName(title),
} as ListItemProps;
};
const groups = groupedJsOperations.map(
({ className, operations, title }) => ({
groupTitle: title,
className: className,
items: operations.map(getListItems),
}),
);
const localGroups = fuzzySearchInObjectItems<GroupedListProps[]>(
searchTerm,
groups,
);
return (
<Flex
data-testid="t--ide-add-pane"
@ -61,14 +81,13 @@ const AddJS = ({ containerProps, innerContainerProps }: AddProps) => {
onCloseClick={closeAddJS}
titleMessage={EDITOR_PANE_TEXTS.js_create_tab_title}
/>
<GroupedList
groups={groupedJsOperations.map((op) => ({
groupTitle: op.title,
className: op.className,
items: op.operations.map(getListItems),
}))}
/>
<SearchInput onChange={setSearchTerm} value={searchTerm} />
{localGroups.length > 0 ? <GroupedList groups={localGroups} /> : null}
{localGroups.length === 0 && searchTerm !== "" ? (
<EmptySearchResult
type={createMessage(EDITOR_PANE_TEXTS.search_objects.jsObject)}
/>
) : null}
</Flex>
</Flex>
);

View File

@ -3,6 +3,7 @@ import { useSelector } from "react-redux";
import { Flex, Text } from "design-system";
import styled from "styled-components";
import type { EditorSegmentList } from "@appsmith/selectors/appIDESelectors";
import { selectJSSegmentEditorList } from "@appsmith/selectors/appIDESelectors";
import { useActiveAction } from "@appsmith/pages/Editor/Explorer/hooks";
import {
@ -19,7 +20,8 @@ import { useJSAdd } from "@appsmith/pages/Editor/IDE/EditorPane/JS/hooks";
import { JSListItem } from "@appsmith/pages/Editor/IDE/EditorPane/JS/ListItem";
import { BlankState } from "./BlankState";
import { AddAndSearchbar } from "../components/AddAndSearchbar";
import { fuzzySearchInFiles } from "../utils";
import { fuzzySearchInObjectItems } from "../utils";
import { EmptySearchResult } from "../components/EmptySearchResult";
import { EDITOR_PANE_TEXTS, createMessage } from "@appsmith/constants/messages";
const JSContainer = styled(Flex)`
@ -44,7 +46,10 @@ const ListJSObjects = () => {
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const localFiles = fuzzySearchInFiles(searchTerm, files);
const localFiles = fuzzySearchInObjectItems<EditorSegmentList>(
searchTerm,
files,
);
const canCreateActions = getHasCreateActionPermission(
isFeatureEnabled,
@ -112,13 +117,9 @@ const ListJSObjects = () => {
);
})}
{localFiles.length === 0 && searchTerm !== "" ? (
<Text
className="font-normal text-center"
color="var(--ads-v2-color-fg-muted)"
kind="body-s"
>
{createMessage(EDITOR_PANE_TEXTS.empty_search_result, "JS")}
</Text>
<EmptySearchResult
type={createMessage(EDITOR_PANE_TEXTS.search_objects.jsObject)}
/>
) : null}
</Flex>
</FilesContextProvider>

View File

@ -1,7 +1,7 @@
import React from "react";
import { Flex } from "design-system";
import React, { useState } from "react";
import { Flex, SearchInput } from "design-system";
import { EDITOR_PANE_TEXTS } from "@appsmith/constants/messages";
import { EDITOR_PANE_TEXTS, createMessage } from "@appsmith/constants/messages";
import SegmentAddHeader from "../components/SegmentAddHeader";
import GroupedList from "../components/GroupedList";
import {
@ -10,12 +10,27 @@ import {
useQueryAdd,
} from "@appsmith/pages/Editor/IDE/EditorPane/Query/hooks";
import type { AddProps } from "../types/AddProps";
import { fuzzySearchInObjectItems } from "../utils";
import type { GroupedListProps } from "../components/types";
import { EmptySearchResult } from "../components/EmptySearchResult";
const AddQuery = ({ containerProps, innerContainerProps }: AddProps) => {
const [searchTerm, setSearchTerm] = useState("");
const { getListItems } = useAddQueryListItems();
const groupedActionOperations = useGroupedAddQueryOperations();
const { closeAddQuery } = useQueryAdd();
const groups = groupedActionOperations.map((group) => ({
groupTitle: group.title,
className: group.className,
items: getListItems(group.operations),
}));
const localGroups = fuzzySearchInObjectItems<GroupedListProps[]>(
searchTerm,
groups,
);
return (
<Flex
data-testid="t--ide-add-pane"
@ -35,13 +50,13 @@ const AddQuery = ({ containerProps, innerContainerProps }: AddProps) => {
onCloseClick={closeAddQuery}
titleMessage={EDITOR_PANE_TEXTS.query_create_tab_title}
/>
<GroupedList
groups={groupedActionOperations.map((group) => ({
groupTitle: group.title,
className: group.className,
items: getListItems(group.operations),
}))}
/>
<SearchInput autofocus onChange={setSearchTerm} value={searchTerm} />
{localGroups.length > 0 ? <GroupedList groups={localGroups} /> : null}
{localGroups.length === 0 && searchTerm !== "" ? (
<EmptySearchResult
type={createMessage(EDITOR_PANE_TEXTS.search_objects.datasources)}
/>
) : null}
</Flex>
</Flex>
);

View File

@ -11,6 +11,7 @@ import {
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { getCurrentPageId } from "@appsmith/selectors/entitiesSelector";
import type { EditorSegmentList } from "@appsmith/selectors/appIDESelectors";
import { selectQuerySegmentEditorList } from "@appsmith/selectors/appIDESelectors";
import { ActionParentEntityType } from "@appsmith/entities/Engine/actionHelpers";
import { FilesContextProvider } from "pages/Editor/Explorer/Files/FilesContextProvider";
@ -19,7 +20,8 @@ import { QueryListItem } from "@appsmith/pages/Editor/IDE/EditorPane/Query/ListI
import { getShowWorkflowFeature } from "@appsmith/selectors/workflowSelectors";
import { BlankState } from "./BlankState";
import { AddAndSearchbar } from "../components/AddAndSearchbar";
import { fuzzySearchInFiles } from "../utils";
import { fuzzySearchInObjectItems } from "../utils";
import { EmptySearchResult } from "../components/EmptySearchResult";
import { EDITOR_PANE_TEXTS, createMessage } from "@appsmith/constants/messages";
const ListQuery = () => {
@ -30,7 +32,10 @@ const ListQuery = () => {
const pagePermissions = useSelector(getPagePermissions);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const localFiles = fuzzySearchInFiles(searchTerm, files);
const localFiles = fuzzySearchInObjectItems<EditorSegmentList>(
searchTerm,
files,
);
const canCreateActions = getHasCreateActionPermission(
isFeatureEnabled,
@ -92,13 +97,9 @@ const ListQuery = () => {
);
})}
{localFiles.length === 0 && searchTerm !== "" ? (
<Text
className="font-normal text-center"
color="var(--ads-v2-color-fg-muted)"
kind="body-s"
>
{createMessage(EDITOR_PANE_TEXTS.empty_search_result, "queries")}
</Text>
<EmptySearchResult
type={createMessage(EDITOR_PANE_TEXTS.search_objects.jsObject)}
/>
) : null}
</Flex>

View File

@ -0,0 +1,17 @@
import React from "react";
import { Text } from "design-system";
import { EDITOR_PANE_TEXTS, createMessage } from "@appsmith/constants/messages";
const EmptySearchResult = ({ type }: { type: string }) => {
return (
<Text
className="font-normal text-center"
color="var(--ads-v2-color-fg-muted)"
kind="body-s"
>
{createMessage(EDITOR_PANE_TEXTS.empty_search_result, type)}
</Text>
);
};
export { EmptySearchResult };

View File

@ -0,0 +1,81 @@
import React, { useMemo, useState } from "react";
import type { GroupedListProps } from "./types";
import { DEFAULT_GROUP_LIST_SIZE } from "./constants";
import { Flex, List, Text } from "design-system";
import styled from "styled-components";
interface GroupProps {
group: GroupedListProps;
}
const StyledList = styled(List)`
padding: 0;
gap: 0;
& .ds-load-more .ads-v2-listitem__title {
--color: var(--ads-v2-color-fg-subtle);
}
& .ads-v2-listitem__wrapper .ads-v2-listitem__idesc {
opacity: 0;
}
& .ads-v2-listitem__wrapper:hover .ads-v2-listitem__idesc {
opacity: 1;
}
`;
const Group: React.FC<GroupProps> = ({ group }) => {
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(
DEFAULT_GROUP_LIST_SIZE,
);
const { className, groupTitle, items: groupItems } = group;
const items = useMemo(() => {
const items = groupItems.slice(0, visibleItemsCount);
const hasMoreItems = groupItems.length > visibleItemsCount;
const handleLoadMore = () => {
setVisibleItemsCount(groupItems.length);
};
if (hasMoreItems) {
items.push({
title: "Load more...",
description: "",
descriptionType: "inline",
onClick: handleLoadMore,
className: "ds-load-more",
});
}
// TODO: try to avoid this
if (hasMoreItems && groupTitle === "Datasources") {
items.push(groupItems[groupItems.length - 1]);
}
return items;
}, [groupItems, visibleItemsCount, groupTitle]);
return (
<Flex
borderBottom="1px solid var(--ads-v2-color-border-muted)"
className="groups-list-group"
flexDirection="column"
key={groupTitle}
pb="spaces-3"
>
{groupTitle ? (
<Text
className="px-0 py-[var(--ads-v2-spaces-1)]"
color="var(--ads-v2-color-fg-muted)"
kind="body-s"
>
{groupTitle}
</Text>
) : null}
<StyledList className={className} items={items} />
</Flex>
);
};
export { Group };

View File

@ -1,27 +1,24 @@
import React from "react";
import type { FlexProps } from "design-system";
import { Flex } from "design-system";
import styled from "styled-components";
import type { FlexProps, ListItemProps } from "design-system";
import { Flex, List, Text } from "design-system";
const StyledList = styled(List)`
padding: 0;
gap: 0;
`;
export type GroupedListProps = Array<{
groupTitle?: string;
className: string;
items: ListItemProps[];
}>;
import type { GroupedListProps } from "./types";
import { Group } from "./Group";
interface Props {
groups: GroupedListProps;
groups: GroupedListProps[];
flexProps?: FlexProps;
}
const StyledFlex = styled(Flex)`
& .groups-list-group:last-child {
border-bottom: none;
}
`;
const GroupedList = (props: Props) => {
return (
<Flex
<StyledFlex
flex="1"
flexDirection="column"
gap="spaces-4"
@ -29,20 +26,9 @@ const GroupedList = (props: Props) => {
{...props.flexProps}
>
{props.groups.map((group) => (
<Flex flexDirection="column" key={group.groupTitle}>
{group.groupTitle ? (
<Text
className="px-0 py-[var(--ads-v2-spaces-1)]"
color="var(--ads-v2-color-fg-muted)"
kind="body-s"
>
{group.groupTitle}
</Text>
) : null}
<StyledList className={group.className} items={group.items} />
</Flex>
<Group group={group} key={group.groupTitle} />
))}
</Flex>
</StyledFlex>
);
};

View File

@ -20,7 +20,6 @@ const SegmentAddHeader = (props: Props) => {
: "var(--ads-v2-color-gray-50)"
}
justifyContent="space-between"
py="spaces-2"
>
<Text color="var(--ads-v2-color-fg)" kind="heading-xs">
{createMessage(props.titleMessage)}

View File

@ -0,0 +1 @@
export const DEFAULT_GROUP_LIST_SIZE = 5;

View File

@ -0,0 +1,7 @@
import type { ListItemProps } from "design-system";
export interface GroupedListProps {
groupTitle?: string;
className: string;
items: ListItemProps[];
}

View File

@ -0,0 +1 @@
export const EDITOR_TABS_HEIGHT = "32px";

View File

@ -1,5 +1,5 @@
import type { EditorSegmentList } from "@appsmith/selectors/appIDESelectors";
import { fuzzySearchInFiles } from "./utils";
import { fuzzySearchInObjectItems } from "./utils";
import { PluginType } from "entities/Action";
const sampleFiles: EditorSegmentList = [
@ -19,14 +19,14 @@ const sampleFiles: EditorSegmentList = [
},
];
describe("fuzzySearchInFiles", () => {
describe("fuzzySearchInObjectItems", () => {
it("should return all files when the search string is empty", () => {
const result = fuzzySearchInFiles("", sampleFiles);
const result = fuzzySearchInObjectItems("", sampleFiles);
expect(result).toEqual(sampleFiles);
});
it("should return the correct file when the search string exactly matches a file title", () => {
const result = fuzzySearchInFiles("file1", sampleFiles);
const result = fuzzySearchInObjectItems("file1", sampleFiles);
expect(result).toEqual([
{
group: "Group 1",
@ -36,12 +36,12 @@ describe("fuzzySearchInFiles", () => {
});
it("should return an empty array when no files match the search string", () => {
const result = fuzzySearchInFiles("nonexistentfile", sampleFiles);
const result = fuzzySearchInObjectItems("nonexistentfile", sampleFiles);
expect(result).toEqual([]);
});
it("should return all files containing the common substring in their titles", () => {
const result = fuzzySearchInFiles("file", sampleFiles);
const result = fuzzySearchInObjectItems("file", sampleFiles);
expect(result).toEqual(sampleFiles);
});
});

View File

@ -1,5 +1,4 @@
import Fuse from "fuse.js";
import type { EditorSegmentList } from "@appsmith/selectors/appIDESelectors";
export const createAddClassName = (name: string) => {
return "t--datasoucre-create-option-" + name.toLowerCase().replace(/ /g, "_");
@ -11,19 +10,20 @@ const FUSE_OPTIONS = {
keys: ["title"],
};
export const fuzzySearchInFiles = (
export const fuzzySearchInObjectItems = <T extends any[]>(
searchStr: string,
files: EditorSegmentList,
) => {
files: T,
): T => {
if (searchStr && searchStr !== "") {
const newFiles = files
.map((group) => {
const fuse = new Fuse(group.items, FUSE_OPTIONS);
const items = group["items"];
const fuse = new Fuse(items, FUSE_OPTIONS);
const resultItems = fuse.search(searchStr);
return { ...group, items: resultItems };
})
.filter((group) => group.items.length > 0);
return newFiles;
return newFiles as T;
}
return files;

View File

@ -1,6 +1,7 @@
import type { ReactNode } from "react";
import React from "react";
import { Flex } from "design-system";
import { EDITOR_TABS_HEIGHT } from "../EditorPane/constants";
const Container = (props: { children: ReactNode }) => {
return (
@ -10,8 +11,8 @@ const Container = (props: { children: ReactNode }) => {
borderBottom="1px solid var(--ads-v2-color-border-muted)"
gap="spaces-2"
id="ide-tabs-container"
maxHeight="32px"
minHeight="32px"
maxHeight={EDITOR_TABS_HEIGHT}
minHeight={EDITOR_TABS_HEIGHT}
px="spaces-2"
width="100%"
>

View File

@ -1,10 +1,11 @@
import React from "react";
import { Flex } from "design-system";
import AddJS from "pages/Editor/IDE/EditorPane/JS/Add";
import { EDITOR_TABS_HEIGHT } from "../IDE/EditorPane/constants";
const JSAddState = () => {
return (
<Flex height="100%" justifyContent="center">
<Flex height={`calc(100% - ${EDITOR_TABS_HEIGHT})`} justifyContent="center">
<AddJS
containerProps={{
px: "spaces-4",

View File

@ -1,10 +1,11 @@
import React from "react";
import { Flex } from "design-system";
import AddQuery from "pages/Editor/IDE/EditorPane/Query/Add";
import { EDITOR_TABS_HEIGHT } from "../IDE/EditorPane/constants";
const QueriesAddState = () => {
return (
<Flex height="100%" justifyContent="center">
<Flex height={`calc(100% - ${EDITOR_TABS_HEIGHT})`} justifyContent="center">
<AddQuery
containerProps={{
px: "spaces-4",