Merge branch 'release' of https://github.com/appsmithorg/appsmith into feat/reactive-actions-run-behaviour
This commit is contained in:
commit
f61747a597
21
CODEOWNERS
21
CODEOWNERS
|
|
@ -191,16 +191,21 @@ app/client/src/ce/JSFunctionExecutionSaga.ts @ApekshaBhosale
|
|||
app/client/src/ee/JSFunctionExecutionSaga.ts @ApekshaBhosale
|
||||
|
||||
# Enterprise Success
|
||||
app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/**/* @sharat87 @abhvsn @AnaghHegde
|
||||
app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/**/* @sharat87 @abhvsn
|
||||
|
||||
# DevOps & Shri
|
||||
deploy/**/* @sharat87 @pratapaprasanna
|
||||
.github/workflows/*.yml @sharat87
|
||||
app/client/packages/ctl/**/* @sharat87 @pratapaprasanna
|
||||
app/server/**/pom.xml @sharat87
|
||||
# DevOps
|
||||
deploy/**/* @sharat87 @pratapaprasanna @nidhi-nair
|
||||
.github/workflows/*.yml @sharat87 @nidhi-nair
|
||||
app/client/packages/ctl/**/* @sharat87 @pratapaprasanna @nidhi-nair
|
||||
Dockerfile @nidhi-nair
|
||||
|
||||
# Server dependencies
|
||||
app/server/**/pom.xml @sharat87 @nidhi-nair
|
||||
|
||||
# Repository layer
|
||||
app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/BaseAppsmithRepositoryCEImpl.java @sharat87
|
||||
app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/ce/params/QueryAllParams.java @sharat87
|
||||
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/bridge/**/* @sharat87
|
||||
|
||||
#Cypress
|
||||
app/client/cypress/**/* @ApekshaBhosale @sagar-qa007
|
||||
# Cypress
|
||||
app/client/cypress/**/* @ApekshaBhosale
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const isEllipsisActive = (element: HTMLElement | null) => {
|
|||
export const EditableEntityName = (props: EditableEntityNameProps) => {
|
||||
const {
|
||||
canEdit,
|
||||
hasError,
|
||||
icon,
|
||||
inputTestId,
|
||||
isEditing,
|
||||
|
|
@ -106,6 +107,7 @@ export const EditableEntityName = (props: EditableEntityNameProps) => {
|
|||
<Styled.Text
|
||||
aria-invalid={Boolean(validationError)}
|
||||
className={clsx("t--entity-name", { editing: inEditMode })}
|
||||
color={hasError ? "var(--ads-v2-color-fg-error)" : undefined}
|
||||
data-isediting={inEditMode}
|
||||
data-isfixedwidth={isFixedWidth}
|
||||
inputProps={inputProps}
|
||||
|
|
|
|||
|
|
@ -27,4 +27,6 @@ export interface EditableEntityNameProps {
|
|||
normalizeName?: boolean;
|
||||
/** Used for showing ellipsis for longer names */
|
||||
showEllipsis?: boolean;
|
||||
/** Whether to show the entity is in error state */
|
||||
hasError?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const EntityItem = (props: EntityItemProps) => {
|
|||
return (
|
||||
<EditableEntityName
|
||||
canEdit={canEdit}
|
||||
hasError={props.hasError}
|
||||
icon={startIcon}
|
||||
isEditing={isEditing}
|
||||
isFixedWidth
|
||||
|
|
@ -50,6 +51,7 @@ export const EntityItem = (props: EntityItemProps) => {
|
|||
normalizeName,
|
||||
onEditComplete,
|
||||
onNameSave,
|
||||
props.hasError,
|
||||
props.title,
|
||||
startIcon,
|
||||
validateName,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface EntityListTreeItem {
|
|||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
hasError?: boolean;
|
||||
}
|
||||
|
||||
export interface EntityListTreeProps {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
|||
import { Text } from "@appsmith/ads";
|
||||
import { useIsEditorInitialised } from "IDE/hooks";
|
||||
import { useActionSettingsConfig } from "./hooks";
|
||||
import { createMessage, PLUGIN_NOT_INSTALLED } from "ee/constants/messages";
|
||||
import { ShowUpgradeMenuItem } from "ee/utils/licenseHelpers";
|
||||
|
||||
interface ChildrenProps {
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
|
|
@ -54,10 +56,11 @@ const PluginActionEditor = (props: ChildrenProps) => {
|
|||
|
||||
if (!plugin) {
|
||||
return (
|
||||
<CenteredWrapper>
|
||||
<CenteredWrapper className="flex-col">
|
||||
<Text color="var(--ads-v2-color-fg-error)" kind="heading-m">
|
||||
Plugin not installed!
|
||||
{createMessage(PLUGIN_NOT_INSTALLED)}
|
||||
</Text>
|
||||
<ShowUpgradeMenuItem />
|
||||
</CenteredWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { DatasourceConnectionMode } from "entities/Datasource";
|
||||
import Snowflake from ".";
|
||||
import { SSLType } from "entities/Datasource/RestAPIForm";
|
||||
|
||||
describe("Snowflake WidgetQueryGenerator", () => {
|
||||
const initialValues = {
|
||||
|
|
@ -367,4 +368,36 @@ OFFSET
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should return provided connection mode when available", () => {
|
||||
const datasourceConfiguration = {
|
||||
connection: {
|
||||
mode: DatasourceConnectionMode.READ_ONLY,
|
||||
ssl: {
|
||||
authType: SSLType.DEFAULT,
|
||||
authTypeControl: false,
|
||||
certificateFile: {
|
||||
name: "",
|
||||
base64Content: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
url: "https://example.com",
|
||||
};
|
||||
|
||||
const connectionMode = Snowflake.getConnectionMode(datasourceConfiguration);
|
||||
|
||||
expect(connectionMode).toEqual(DatasourceConnectionMode.READ_ONLY);
|
||||
});
|
||||
|
||||
test("should return READ_WRITE as default when no connection mode is provided", () => {
|
||||
const datasourceConfiguration = {
|
||||
// No connection mode specified
|
||||
url: "https://example.com",
|
||||
};
|
||||
|
||||
const connectionMode = Snowflake.getConnectionMode(datasourceConfiguration);
|
||||
|
||||
expect(connectionMode).toEqual(DatasourceConnectionMode.READ_WRITE);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { BaseQueryGenerator } from "../BaseQueryGenerator";
|
||||
import {
|
||||
DatasourceConnectionMode,
|
||||
type DatasourceStorage,
|
||||
} from "entities/Datasource";
|
||||
import { without } from "lodash";
|
||||
import { formatDialect, snowflake } from "sql-formatter";
|
||||
import { QUERY_TYPE } from "../types";
|
||||
import { removeSpecialChars } from "utils/helpers";
|
||||
import { BaseQueryGenerator } from "../BaseQueryGenerator";
|
||||
import type {
|
||||
ActionConfigurationSQL,
|
||||
WidgetQueryGenerationConfig,
|
||||
WidgetQueryGenerationFormConfig,
|
||||
ActionConfigurationSQL,
|
||||
} from "../types";
|
||||
import { removeSpecialChars } from "utils/helpers";
|
||||
import { without } from "lodash";
|
||||
import { DatasourceConnectionMode } from "entities/Datasource";
|
||||
import { QUERY_TYPE } from "../types";
|
||||
|
||||
export default abstract class Snowflake extends BaseQueryGenerator {
|
||||
private static buildSelect(
|
||||
|
|
@ -249,4 +252,13 @@ export default abstract class Snowflake extends BaseQueryGenerator {
|
|||
static getTotalRecordExpression(binding: string) {
|
||||
return `${binding}[0].count`;
|
||||
}
|
||||
|
||||
static getConnectionMode(
|
||||
datasourceConfiguration: DatasourceStorage["datasourceConfiguration"],
|
||||
) {
|
||||
return (
|
||||
datasourceConfiguration?.connection?.mode ||
|
||||
DatasourceConnectionMode.READ_WRITE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2002,10 +2002,12 @@ export const IN_APP_EMBED_SETTING = {
|
|||
"Make your app public to embed your Appsmith app into legacy applications",
|
||||
secondaryHeading: () =>
|
||||
"Embedding in public mode is supported in the free plan. To make your app public, please contact your administrator.",
|
||||
chromeExtensionBannerTitle: () => "Get the Chrome extension",
|
||||
chromeExtensionBannerTitle: (isInstalled: boolean) =>
|
||||
isInstalled ? "Appsmith Agents extension" : "Install the Chrome extension",
|
||||
chromeExtensionBannerDescription: () =>
|
||||
"Bring powerful AI assistance to the tools you and your teams use.",
|
||||
chromeExtensionBannerButton: () => "Get the extension",
|
||||
chromeExtensionBannerButton: (isInstalled: boolean) =>
|
||||
isInstalled ? "Extension settings" : "Get the extension",
|
||||
};
|
||||
|
||||
export const APP_NAVIGATION_SETTING = {
|
||||
|
|
@ -2691,3 +2693,6 @@ export const GOOGLE_RECAPTCHA_FAILED = () =>
|
|||
"Google reCAPTCHA verification failed";
|
||||
export const PASSWORD_INSUFFICIENT_STRENGTH = () =>
|
||||
"Insufficient password strength";
|
||||
|
||||
export const PLUGIN_NOT_INSTALLED = () =>
|
||||
"Upgrade your plan to unlock access to these integrations.";
|
||||
|
|
|
|||
|
|
@ -45,34 +45,40 @@ export const useAddQueryListItems = () => {
|
|||
);
|
||||
|
||||
const getListItems = (data: ActionOperation[]) => {
|
||||
return data.map((fileOperation) => {
|
||||
let title =
|
||||
fileOperation.entityExplorerTitle ||
|
||||
fileOperation.dsName ||
|
||||
fileOperation.title;
|
||||
return data
|
||||
.map((fileOperation) => {
|
||||
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 ||
|
||||
(fileOperation.pluginId &&
|
||||
getPluginEntityIcon(pluginGroups[fileOperation.pluginId]));
|
||||
|
||||
return {
|
||||
startIcon: icon,
|
||||
className: className,
|
||||
title,
|
||||
description:
|
||||
title =
|
||||
fileOperation.focusEntityType === FocusEntity.QUERY_MODULE_INSTANCE
|
||||
? fileOperation.dsName
|
||||
: "",
|
||||
descriptionType: "inline",
|
||||
onClick: onCreateItemClick.bind(null, fileOperation),
|
||||
} as ListItemProps;
|
||||
});
|
||||
? fileOperation.title
|
||||
: title;
|
||||
const className = createAddClassName(title);
|
||||
const icon =
|
||||
fileOperation.icon ||
|
||||
(fileOperation.pluginId &&
|
||||
getPluginEntityIcon(pluginGroups[fileOperation.pluginId]));
|
||||
|
||||
if (fileOperation.pluginId && !pluginGroups[fileOperation.pluginId]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
startIcon: icon,
|
||||
className: className,
|
||||
title,
|
||||
description:
|
||||
fileOperation.focusEntityType === FocusEntity.QUERY_MODULE_INSTANCE
|
||||
? fileOperation.dsName
|
||||
: "",
|
||||
descriptionType: "inline",
|
||||
onClick: onCreateItemClick.bind(null, fileOperation),
|
||||
} as ListItemProps;
|
||||
})
|
||||
.filter((item) => item !== undefined);
|
||||
};
|
||||
|
||||
return { getListItems };
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ export const useFilteredFileOperations = ({
|
|||
}: FilterFileOperationsProps) => {
|
||||
const { appWideDS = [], otherDS = [] } = useAppWideAndOtherDatasource();
|
||||
const plugins = useSelector(getPlugins);
|
||||
const pluginById = useMemo(() => {
|
||||
return keyBy(plugins, "id");
|
||||
}, [plugins]);
|
||||
const moduleOptions = useModuleOptions();
|
||||
const workflowOptions = useWorkflowOptions();
|
||||
|
||||
|
|
@ -106,7 +109,7 @@ export const useFilteredFileOperations = ({
|
|||
return AiPlugin.id !== ds.pluginId;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !!pluginById[ds.pluginId]?.id;
|
||||
});
|
||||
|
||||
return useFilteredAndSortedFileOperations({
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import type { ExplorerURLParams } from "ee/pages/Editor/Explorer/helpers";
|
|||
import { getLastSelectedWidget } from "selectors/ui";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import useRecentEntities from "./useRecentEntities";
|
||||
import { noop } from "lodash";
|
||||
import { keyBy, noop } from "lodash";
|
||||
import {
|
||||
getCurrentPageId,
|
||||
getPagePermissions,
|
||||
|
|
@ -179,6 +179,9 @@ function GlobalSearch() {
|
|||
(state: AppState) => state.ui.globalSearch.filterContext.category,
|
||||
);
|
||||
const plugins = useSelector(getPlugins);
|
||||
const pluginById = useMemo(() => {
|
||||
return keyBy(plugins, "id");
|
||||
}, [plugins]);
|
||||
const setCategory = useCallback(
|
||||
(category: SearchCategory) => {
|
||||
dispatch(setGlobalSearchFilterContext({ category: category }));
|
||||
|
|
@ -233,10 +236,15 @@ function GlobalSearch() {
|
|||
}, [basePageIdToPageIdMap, params?.basePageId, reducerDatasources]);
|
||||
|
||||
const filteredDatasources = useMemo(() => {
|
||||
if (!query) return datasourcesList;
|
||||
if (!query)
|
||||
return datasourcesList.filter(
|
||||
(datasource) => pluginById[datasource.pluginId]?.id,
|
||||
);
|
||||
|
||||
return datasourcesList.filter((datasource) =>
|
||||
isMatching(datasource.name, query),
|
||||
return datasourcesList.filter(
|
||||
(datasource) =>
|
||||
isMatching(datasource.name, query) &&
|
||||
pluginById[datasource.pluginId]?.id,
|
||||
);
|
||||
}, [datasourcesList, query]);
|
||||
const recentEntities = useRecentEntities();
|
||||
|
|
|
|||
|
|
@ -22,27 +22,42 @@ const IconContainer = styled.div`
|
|||
align-items: center;
|
||||
`;
|
||||
|
||||
const Label = styled.div`
|
||||
const LabelContainer = styled.div`
|
||||
width: calc(100% - 40px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Label = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const SubText = styled.span`
|
||||
font-size: 12px;
|
||||
color: var(--ads-v2-color-fg-muted);
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
label?: JSX.Element | string;
|
||||
leftIcon?: JSX.Element;
|
||||
rightIcon?: JSX.Element;
|
||||
className?: string;
|
||||
subText?: string;
|
||||
}
|
||||
|
||||
export function DropdownOption(props: Props) {
|
||||
const { className, label, leftIcon, rightIcon } = props;
|
||||
const { className, label, leftIcon, rightIcon, subText } = props;
|
||||
|
||||
return (
|
||||
<Container className={className}>
|
||||
<LeftSection>
|
||||
{leftIcon && <IconContainer>{leftIcon}</IconContainer>}
|
||||
<Label>{label}</Label>
|
||||
<LabelContainer>
|
||||
<Label>{label}</Label>
|
||||
{subText && <SubText>{subText}</SubText>}
|
||||
</LabelContainer>
|
||||
</LeftSection>
|
||||
{rightIcon && <IconContainer>{rightIcon}</IconContainer>}
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import React, { memo, useContext } from "react";
|
||||
import { ErrorMessage, Label, LabelWrapper, SelectWrapper } from "../../styles";
|
||||
import {
|
||||
ErrorMessage,
|
||||
Label,
|
||||
LabelWrapper,
|
||||
PrimaryKeysMessage,
|
||||
SelectWrapper,
|
||||
} from "../../styles";
|
||||
import { useTableOrSpreadsheet } from "./useTableOrSpreadsheet";
|
||||
import { Select, Option, Tooltip } from "@appsmith/ads";
|
||||
import { DropdownOption } from "../DatasourceDropdown/DropdownOption";
|
||||
import type { DefaultOptionType } from "rc-select/lib/Select";
|
||||
import { ColumnSelectorModal } from "../ColumnSelectorModal";
|
||||
import { WidgetQueryGeneratorFormContext } from "components/editorComponents/WidgetQueryGeneratorForm/index";
|
||||
import { createMessage } from "ee/constants/messages";
|
||||
import { NO_PRIMARY_KEYS_MESSAGE, PRIMARY_KEYS_MESSAGE } from "../../constants";
|
||||
|
||||
function TableOrSpreadsheetDropdown() {
|
||||
const {
|
||||
|
|
@ -55,10 +63,18 @@ function TableOrSpreadsheetDropdown() {
|
|||
return (
|
||||
<Option
|
||||
data-testid="t--one-click-binding-table-selector--table"
|
||||
disabled={option.disabled}
|
||||
key={option.id}
|
||||
value={option.value}
|
||||
>
|
||||
<DropdownOption label={option.label} />
|
||||
<DropdownOption
|
||||
label={option.label}
|
||||
subText={
|
||||
option.disabled
|
||||
? createMessage(NO_PRIMARY_KEYS_MESSAGE)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
|
|
@ -66,6 +82,10 @@ function TableOrSpreadsheetDropdown() {
|
|||
<ErrorMessage data-testid="t--one-click-binding-table-selector--error">
|
||||
{error}
|
||||
</ErrorMessage>
|
||||
|
||||
<PrimaryKeysMessage>
|
||||
{createMessage(PRIMARY_KEYS_MESSAGE)}
|
||||
</PrimaryKeysMessage>
|
||||
</SelectWrapper>
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,326 @@
|
|||
import { useTableOrSpreadsheet } from "./useTableOrSpreadsheet";
|
||||
import { renderHook } from "@testing-library/react-hooks";
|
||||
import type { DatasourceTable } from "entities/Datasource";
|
||||
import { PluginPackageName } from "entities/Plugin";
|
||||
|
||||
// Mock pageListSelectors
|
||||
jest.mock("selectors/pageListSelectors", () => ({
|
||||
getIsGeneratingTemplatePage: jest.fn(() => false),
|
||||
getIsGeneratePageModalOpen: jest.fn(() => false),
|
||||
getApplicationLastModifiedTime: jest.fn(),
|
||||
getCurrentApplicationId: jest.fn(),
|
||||
getCurrentPageId: jest.fn(),
|
||||
getPageById: jest.fn(),
|
||||
getPageList: jest.fn(() => []),
|
||||
getPageListAsOptions: jest.fn(() => []),
|
||||
getPageListState: jest.fn(() => ({
|
||||
isGeneratingTemplatePage: false,
|
||||
isGeneratePageModalOpen: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock UI selectors
|
||||
jest.mock("selectors/ui", () => ({
|
||||
getSelectedAppTheme: jest.fn(),
|
||||
getAppThemes: jest.fn(() => []),
|
||||
getSelectedAppThemeColor: jest.fn(),
|
||||
getIsDatasourceInViewMode: jest.fn(() => false),
|
||||
getDatasourceCollapsibleState: jest.fn(() => ({})),
|
||||
getIsInOnboardingFlow: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
// Mock datasourceActions
|
||||
jest.mock("actions/datasourceActions", () => ({
|
||||
fetchGheetSheets: jest.fn(() => ({ type: "FETCH_GSHEET_SHEETS" })),
|
||||
}));
|
||||
|
||||
// Mock editor selectors
|
||||
jest.mock("selectors/editorSelectors", () => ({
|
||||
getWidgets: jest.fn(() => ({})),
|
||||
getWidgetsMeta: jest.fn(() => ({})),
|
||||
getWidgetsForImport: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
// Mock Reselect
|
||||
jest.mock("reselect", () => ({
|
||||
createSelector: jest.fn((selectors, resultFunc) => {
|
||||
if (typeof resultFunc === "function") {
|
||||
return resultFunc();
|
||||
}
|
||||
|
||||
return jest.fn();
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock selectors
|
||||
jest.mock("selectors/dataTreeSelectors", () => ({
|
||||
getLayoutSystemType: jest.fn(),
|
||||
getIsMobileBreakPoint: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock redux
|
||||
jest.mock("react-redux", () => ({
|
||||
useSelector: jest.fn(),
|
||||
useDispatch: () => jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock redux-form
|
||||
jest.mock("redux-form", () => ({
|
||||
getFormValues: jest.fn(),
|
||||
Field: jest.fn(),
|
||||
reduxForm: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock ee/selectors/entitiesSelector
|
||||
jest.mock("ee/selectors/entitiesSelector", () => ({
|
||||
getDatasource: jest.fn(),
|
||||
getDatasourceLoading: jest.fn(),
|
||||
getDatasourceStructureById: jest.fn(),
|
||||
getIsFetchingDatasourceStructure: jest.fn(),
|
||||
getPluginPackageFromDatasourceId: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock selectors/datasourceSelectors
|
||||
jest.mock("selectors/datasourceSelectors", () => ({
|
||||
getGsheetSpreadsheets: jest.fn(() => jest.fn()),
|
||||
getIsFetchingGsheetSpreadsheets: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock selectors/oneClickBindingSelectors
|
||||
jest.mock("selectors/oneClickBindingSelectors", () => ({
|
||||
getisOneClickBindingConnectingForWidget: jest.fn(() => jest.fn()),
|
||||
}));
|
||||
|
||||
// Mock sagas/selectors
|
||||
jest.mock("sagas/selectors", () => ({
|
||||
getWidget: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock utils
|
||||
jest.mock("utils/editorContextUtils", () => ({
|
||||
isGoogleSheetPluginDS: jest.fn(
|
||||
(plugin) => plugin === PluginPackageName.GOOGLE_SHEETS,
|
||||
),
|
||||
isMongoDBPluginDS: jest.fn((plugin) => plugin === PluginPackageName.MONGO),
|
||||
}));
|
||||
|
||||
// Mock utils/helpers
|
||||
jest.mock("utils/helpers", () => ({
|
||||
getAppMode: jest.fn(),
|
||||
isEllipsisActive: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock WidgetOperationUtils
|
||||
jest.mock("sagas/WidgetOperationUtils", () => ({}));
|
||||
|
||||
// Mock WidgetUtils
|
||||
jest.mock("widgets/WidgetUtils", () => ({}));
|
||||
|
||||
// Mock the context
|
||||
jest.mock("react", () => {
|
||||
const originalModule = jest.requireActual("react");
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
useContext: () => ({
|
||||
config: { datasource: "test-ds-id", table: "" },
|
||||
propertyName: "test-property",
|
||||
updateConfig: jest.fn(),
|
||||
widgetId: "test-widget-id",
|
||||
}),
|
||||
useCallback: <T extends (...args: unknown[]) => unknown>(fn: T) => fn,
|
||||
// useMemo will be mocked in each test
|
||||
useMemo: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Import after all mocks are set up
|
||||
import { useSelector } from "react-redux";
|
||||
import * as React from "react";
|
||||
|
||||
// Create a simplified test
|
||||
describe("useTableOrSpreadsheet", () => {
|
||||
const mockUseSelector = useSelector as jest.Mock;
|
||||
const mockUseMemo = React.useMemo as jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseSelector.mockImplementation(() => ({}));
|
||||
mockUseMemo.mockImplementation((fn) => fn());
|
||||
});
|
||||
|
||||
it("should render without crashing", () => {
|
||||
// Mock minimum required values
|
||||
mockUseSelector.mockImplementation(() => ({
|
||||
datasourceStructure: { tables: [] },
|
||||
selectedDatasourcePluginPackageName: PluginPackageName.POSTGRES,
|
||||
selectedDatasource: { name: "Test" },
|
||||
widget: { widgetName: "Test" },
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useTableOrSpreadsheet());
|
||||
|
||||
expect(result.current).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should disable tables without primary keys for non-MongoDB datasources", () => {
|
||||
const mockTables: DatasourceTable[] = [
|
||||
{
|
||||
name: "table1",
|
||||
type: "TABLE",
|
||||
columns: [],
|
||||
keys: [],
|
||||
templates: [],
|
||||
},
|
||||
{
|
||||
name: "table2",
|
||||
type: "TABLE",
|
||||
columns: [],
|
||||
keys: [
|
||||
{
|
||||
name: "id",
|
||||
type: "primary",
|
||||
columnNames: ["id"],
|
||||
fromColumns: ["id"],
|
||||
},
|
||||
],
|
||||
templates: [],
|
||||
},
|
||||
];
|
||||
|
||||
const mockOptions = [
|
||||
{
|
||||
id: "table1",
|
||||
label: "table1",
|
||||
value: "table1",
|
||||
data: { tableName: "table1" },
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: "table2",
|
||||
label: "table2",
|
||||
value: "table2",
|
||||
data: { tableName: "table2" },
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
mockUseSelector.mockImplementation(() => ({
|
||||
selectedDatasourcePluginPackageName: PluginPackageName.POSTGRES,
|
||||
datasourceStructure: { tables: mockTables },
|
||||
widget: { widgetName: "TestWidget" },
|
||||
selectedDatasource: { name: "TestDatabase" },
|
||||
}));
|
||||
|
||||
mockUseMemo.mockImplementation(() => mockOptions);
|
||||
|
||||
const { result } = renderHook(() => useTableOrSpreadsheet());
|
||||
|
||||
expect(result.current.options).toHaveLength(2);
|
||||
expect(result.current.options[0].value).toBe("table1");
|
||||
expect(result.current.options[0].disabled).toBe(true);
|
||||
expect(result.current.options[1].value).toBe("table2");
|
||||
expect(result.current.options[1].disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should not disable tables for MongoDB datasources regardless of primary keys", () => {
|
||||
const mockTables: DatasourceTable[] = [
|
||||
{
|
||||
name: "collection1",
|
||||
type: "COLLECTION",
|
||||
columns: [],
|
||||
keys: [],
|
||||
templates: [],
|
||||
},
|
||||
{
|
||||
name: "collection2",
|
||||
type: "COLLECTION",
|
||||
columns: [],
|
||||
keys: [
|
||||
{
|
||||
name: "_id",
|
||||
type: "primary",
|
||||
columnNames: ["_id"],
|
||||
fromColumns: ["_id"],
|
||||
},
|
||||
],
|
||||
templates: [],
|
||||
},
|
||||
];
|
||||
|
||||
const mockOptions = [
|
||||
{
|
||||
id: "collection1",
|
||||
label: "collection1",
|
||||
value: "collection1",
|
||||
data: { tableName: "collection1" },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "collection2",
|
||||
label: "collection2",
|
||||
value: "collection2",
|
||||
data: { tableName: "collection2" },
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
mockUseSelector.mockImplementation(() => ({
|
||||
selectedDatasourcePluginPackageName: PluginPackageName.MONGO,
|
||||
datasourceStructure: { tables: mockTables },
|
||||
widget: { widgetName: "TestWidget" },
|
||||
selectedDatasource: { name: "TestMongoDB" },
|
||||
}));
|
||||
|
||||
mockUseMemo.mockImplementation(() => mockOptions);
|
||||
|
||||
const { result } = renderHook(() => useTableOrSpreadsheet());
|
||||
|
||||
expect(result.current.options).toHaveLength(2);
|
||||
expect(result.current.options[0].value).toBe("collection1");
|
||||
expect(result.current.options[0].disabled).toBe(false);
|
||||
expect(result.current.options[1].value).toBe("collection2");
|
||||
expect(result.current.options[1].disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should not disable tables for Google Sheets datasource", () => {
|
||||
const mockOptions = [
|
||||
{
|
||||
id: "sheet1",
|
||||
label: "Sheet1",
|
||||
value: "Sheet1",
|
||||
data: { tableName: "sheet1" },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: "sheet2",
|
||||
label: "Sheet2",
|
||||
value: "Sheet2",
|
||||
data: { tableName: "sheet2" },
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
mockUseSelector.mockImplementation(() => ({
|
||||
selectedDatasourcePluginPackageName: PluginPackageName.GOOGLE_SHEETS,
|
||||
spreadSheets: {
|
||||
value: [
|
||||
{ label: "Sheet1", value: "sheet1" },
|
||||
{ label: "Sheet2", value: "sheet2" },
|
||||
],
|
||||
},
|
||||
widget: { widgetName: "TestWidget" },
|
||||
selectedDatasource: { name: "TestGoogleSheet" },
|
||||
}));
|
||||
|
||||
mockUseMemo.mockImplementation(() => mockOptions);
|
||||
|
||||
const { result } = renderHook(() => useTableOrSpreadsheet());
|
||||
|
||||
expect(result.current.options).toHaveLength(2);
|
||||
expect(result.current.options[0].value).toBe("Sheet1");
|
||||
expect(result.current.options[0].disabled).toBe(false);
|
||||
expect(result.current.options[1].value).toBe("Sheet2");
|
||||
expect(result.current.options[1].disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import React from "react";
|
||||
import { fetchGheetSheets } from "actions/datasourceActions";
|
||||
import { useCallback, useContext, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import {
|
||||
getDatasource,
|
||||
getDatasourceLoading,
|
||||
|
|
@ -9,21 +7,25 @@ import {
|
|||
getIsFetchingDatasourceStructure,
|
||||
getPluginPackageFromDatasourceId,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import { WidgetQueryGeneratorFormContext } from "../..";
|
||||
import { Bold, Label } from "../../styles";
|
||||
import { PluginFormInputFieldMap } from "../../constants";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import type { DatasourceStructure, DatasourceTable } from "entities/Datasource";
|
||||
import React, { useCallback, useContext, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getWidget } from "sagas/selectors";
|
||||
import {
|
||||
getGsheetSpreadsheets,
|
||||
getIsFetchingGsheetSpreadsheets,
|
||||
} from "selectors/datasourceSelectors";
|
||||
import { isGoogleSheetPluginDS } from "utils/editorContextUtils";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { DropdownOption as Option } from "../DatasourceDropdown/DropdownOption";
|
||||
import type { DropdownOptionType } from "../../types";
|
||||
import { getisOneClickBindingConnectingForWidget } from "selectors/oneClickBindingSelectors";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { getWidget } from "sagas/selectors";
|
||||
import type { DatasourceStructure } from "entities/Datasource";
|
||||
import {
|
||||
isGoogleSheetPluginDS,
|
||||
isMongoDBPluginDS,
|
||||
} from "utils/editorContextUtils";
|
||||
import { WidgetQueryGeneratorFormContext } from "../..";
|
||||
import { PluginFormInputFieldMap } from "../../constants";
|
||||
import { Bold, Label } from "../../styles";
|
||||
import type { DropdownOptionType } from "../../types";
|
||||
import { DropdownOption as Option } from "../DatasourceDropdown/DropdownOption";
|
||||
|
||||
export function useTableOrSpreadsheet() {
|
||||
const dispatch = useDispatch();
|
||||
|
|
@ -63,6 +65,10 @@ export function useTableOrSpreadsheet() {
|
|||
).TABLE
|
||||
: "table";
|
||||
|
||||
const tableHasPrimaryKeys = (table: DatasourceTable) => {
|
||||
return table.keys && table.keys.length > 0;
|
||||
};
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (
|
||||
isGoogleSheetPluginDS(selectedDatasourcePluginPackageName) &&
|
||||
|
|
@ -72,19 +78,35 @@ export function useTableOrSpreadsheet() {
|
|||
id: value,
|
||||
label: label,
|
||||
value: label,
|
||||
disabled: false,
|
||||
data: {
|
||||
tableName: value,
|
||||
},
|
||||
}));
|
||||
} else if (datasourceStructure) {
|
||||
return (datasourceStructure.tables || []).map(({ name }) => ({
|
||||
id: name,
|
||||
label: name,
|
||||
value: name,
|
||||
} else if (isMongoDBPluginDS(selectedDatasourcePluginPackageName)) {
|
||||
return (datasourceStructure.tables || []).map((table) => ({
|
||||
id: table.name,
|
||||
label: table.name,
|
||||
value: table.name,
|
||||
data: {
|
||||
tableName: name,
|
||||
tableName: table.name,
|
||||
},
|
||||
disabled: false,
|
||||
}));
|
||||
} else if (datasourceStructure) {
|
||||
return (datasourceStructure.tables || []).map((table) => {
|
||||
const hasPrimaryKeys = tableHasPrimaryKeys(table);
|
||||
|
||||
return {
|
||||
id: table.name,
|
||||
label: table.name,
|
||||
value: table.name,
|
||||
data: {
|
||||
tableName: table.name,
|
||||
},
|
||||
disabled: !hasPrimaryKeys,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,3 +39,8 @@ export const PluginFormInputFieldMap: Record<
|
|||
};
|
||||
|
||||
export const DEFAULT_QUERY_OPTIONS_COUNTS_TO_SHOW = 4;
|
||||
|
||||
export const PRIMARY_KEYS_MESSAGE = () =>
|
||||
"Tables without primary keys are disabled as they are required for reliable data operations.";
|
||||
|
||||
export const NO_PRIMARY_KEYS_MESSAGE = () => "No primary keys";
|
||||
|
|
|
|||
|
|
@ -66,6 +66,13 @@ export const ErrorMessage = styled.div`
|
|||
margin-top: 5px;
|
||||
`;
|
||||
|
||||
export const PrimaryKeysMessage = styled.div`
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
color: var(--ads-v2-color-fg-subtle);
|
||||
margin-top: 5px;
|
||||
`;
|
||||
|
||||
export const Placeholder = styled.div`
|
||||
color: var(--ads-v2-color-fg-subtle);
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ describe("ComputeTablePropertyControlV2.getInputComputedValue", () => {
|
|||
const tableName = "Table1";
|
||||
const inputVariations = [
|
||||
"currentRow.price",
|
||||
`JSObject1.somefunction(currentRow["id"] || 0)`,
|
||||
`
|
||||
[
|
||||
{
|
||||
|
|
@ -58,4 +59,45 @@ describe("ComputeTablePropertyControlV2.getInputComputedValue", () => {
|
|||
ComputeTablePropertyControlV2.getInputComputedValue(computedValue),
|
||||
).toBe(`{{currentRow.quantity}}{{5}}`);
|
||||
});
|
||||
|
||||
it("3. should handle nested parentheses correctly", () => {
|
||||
const input =
|
||||
"JSObject1.complexFunction(currentRow.id, JSObject2.helperFunction(currentRow.name))";
|
||||
const computedValue = ComputeTablePropertyControlV2.getTableComputeBinding(
|
||||
tableName,
|
||||
input,
|
||||
);
|
||||
|
||||
expect(
|
||||
ComputeTablePropertyControlV2.getInputComputedValue(computedValue),
|
||||
).toBe(`{{${input}}}`);
|
||||
});
|
||||
|
||||
it("4. should handle malformed expressions with unbalanced parentheses", () => {
|
||||
// Create a malformed expression by manually crafting a bad computedValue string
|
||||
// Removing the proper closing parenthesis structure
|
||||
const malformedComputedValue =
|
||||
"{{(() => { const tableData = Table1.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (currentRow.function(unbalanced)) : fallback })";
|
||||
|
||||
// The function should gracefully handle this and return the original string
|
||||
// rather than throwing an error or returning a partial/incorrect result
|
||||
expect(
|
||||
ComputeTablePropertyControlV2.getInputComputedValue(
|
||||
malformedComputedValue,
|
||||
),
|
||||
).toBe(malformedComputedValue);
|
||||
});
|
||||
|
||||
it("5. should correctly parse complex but valid expressions with multiple nested parentheses", () => {
|
||||
const complexInput =
|
||||
"JSObject1.process(currentRow.value1, (currentRow.value2 || getDefault()), JSObject2.format(currentRow.value3))";
|
||||
const computedValue = ComputeTablePropertyControlV2.getTableComputeBinding(
|
||||
tableName,
|
||||
complexInput,
|
||||
);
|
||||
|
||||
expect(
|
||||
ComputeTablePropertyControlV2.getInputComputedValue(computedValue),
|
||||
).toBe(`{{${complexInput}}}`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -161,25 +161,86 @@ class ComputeTablePropertyControlV2 extends BaseControl<ComputeTablePropertyCont
|
|||
|
||||
if (!isComputedValue) return propertyValue;
|
||||
|
||||
// Check if the entire structure of the expression looks valid before attempting to parse
|
||||
if (!this.isLikelyValidComputedValue(propertyValue)) {
|
||||
return propertyValue;
|
||||
}
|
||||
|
||||
// Extract the computation logic from the full binding string
|
||||
// Input example: "{{(() => { const tableData = Table1.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (currentRow.price * 2)) : currentRow.price * 2 })()}}"
|
||||
const mapSignatureIndex = propertyValue.indexOf(MAP_FUNCTION_SIGNATURE);
|
||||
|
||||
// Find the actual computation expression between the map parentheses
|
||||
const computationStart = mapSignatureIndex + MAP_FUNCTION_SIGNATURE.length;
|
||||
const computationEnd = propertyValue.indexOf("))", computationStart);
|
||||
|
||||
const { endIndex, isValid } = this.findMatchingClosingParenthesis(
|
||||
propertyValue,
|
||||
computationStart,
|
||||
);
|
||||
|
||||
// Handle case where no matching closing parenthesis is found
|
||||
if (!isValid) {
|
||||
// If we can't find the proper closing parenthesis, fall back to returning the original value
|
||||
// This prevents errors when the expression is malformed
|
||||
return propertyValue;
|
||||
}
|
||||
|
||||
// Extract the computation expression between the map parentheses
|
||||
// Note: At this point, we're just extracting the raw expression like "currentRow.price * 2"
|
||||
// The actual removal of "currentRow." prefix happens later in JSToString()
|
||||
const computationExpression = propertyValue.substring(
|
||||
computationStart,
|
||||
computationEnd,
|
||||
endIndex,
|
||||
);
|
||||
|
||||
return JSToString(computationExpression);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the computed value string looks structurally valid
|
||||
* This helps catch obviously malformed expressions early
|
||||
*/
|
||||
private static isLikelyValidComputedValue(value: string): boolean {
|
||||
// Check for basic structural elements that should be present
|
||||
const hasOpeningStructure = value.includes("(() => {");
|
||||
const hasTableDataAssignment = value.includes("const tableData =");
|
||||
const hasReturnStatement = value.includes("return tableData.length > 0 ?");
|
||||
const hasClosingStructure = value.includes("})()}}");
|
||||
|
||||
return (
|
||||
hasOpeningStructure &&
|
||||
hasTableDataAssignment &&
|
||||
hasReturnStatement &&
|
||||
hasClosingStructure
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to find the matching closing parenthesis
|
||||
* @param text - The text to search in
|
||||
* @param startIndex - The index after the opening parenthesis
|
||||
* @returns Object containing the index of the matching closing parenthesis and whether it was found
|
||||
*/
|
||||
private static findMatchingClosingParenthesis(
|
||||
text: string,
|
||||
startIndex: number,
|
||||
) {
|
||||
let openParenCount = 1; // Start with 1 for the opening parenthesis
|
||||
|
||||
for (let i = startIndex; i < text.length; i++) {
|
||||
if (text[i] === "(") {
|
||||
openParenCount++;
|
||||
} else if (text[i] === ")") {
|
||||
openParenCount--;
|
||||
|
||||
if (openParenCount === 0) {
|
||||
return { endIndex: i, isValid: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matching closing parenthesis found
|
||||
return { endIndex: text.length, isValid: false };
|
||||
}
|
||||
|
||||
getComputedValue = (value: string, tableName: string) => {
|
||||
// Return raw value if:
|
||||
// 1. The value is not a dynamic binding (not wrapped in {{...}})
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import type { PluginType } from "entities/Plugin";
|
|||
import { useLocation } from "react-router";
|
||||
import { useHeaderActions } from "ee/hooks/datasourceEditorHooks";
|
||||
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
|
||||
import { getPlugin } from "ee/selectors/entitiesSelector";
|
||||
|
||||
export const ActionWrapper = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -118,6 +119,10 @@ export const DSFormHeader = (props: DSFormHeaderProps) => {
|
|||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const ideType = getIDETypeByUrl(location.pathname);
|
||||
const plugin = useSelector((state) =>
|
||||
getPlugin(state, datasource?.pluginId || ""),
|
||||
);
|
||||
const isEditDisabled = !plugin?.id;
|
||||
|
||||
const deleteAction = () => {
|
||||
if (isDeleting) return;
|
||||
|
|
@ -210,8 +215,11 @@ export const DSFormHeader = (props: DSFormHeaderProps) => {
|
|||
)}
|
||||
<Button
|
||||
className="t--edit-datasource"
|
||||
isDisabled={isEditDisabled}
|
||||
kind="secondary"
|
||||
onClick={() => {
|
||||
if (isEditDisabled) return;
|
||||
|
||||
setDatasourceViewMode({
|
||||
datasourceId: datasourceId,
|
||||
viewMode: false,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { getCurrentPageId, getPageList } from "selectors/editorSelectors";
|
|||
import type { Datasource } from "entities/Datasource";
|
||||
import type { EventLocation } from "ee/utils/analyticsUtilTypes";
|
||||
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
|
||||
import { getSelectedTableName } from "ee/selectors/entitiesSelector";
|
||||
import { getPlugin, getSelectedTableName } from "ee/selectors/entitiesSelector";
|
||||
|
||||
interface NewActionButtonProps {
|
||||
datasource?: Datasource;
|
||||
|
|
@ -75,6 +75,11 @@ function NewActionButton(props: NewActionButtonProps) {
|
|||
...pages.filter((p) => p.pageId !== currentPageId),
|
||||
];
|
||||
const queryDefaultTableName = useSelector(getSelectedTableName);
|
||||
const plugin = useSelector((state) =>
|
||||
getPlugin(state, datasource?.pluginId || ""),
|
||||
);
|
||||
|
||||
const isDisabled = !!disabled || !plugin?.id;
|
||||
|
||||
const createQueryAction = useCallback(
|
||||
(pageId: string) => {
|
||||
|
|
@ -106,7 +111,7 @@ function NewActionButton(props: NewActionButtonProps) {
|
|||
|
||||
const handleOnInteraction = useCallback(
|
||||
(open: boolean) => {
|
||||
if (disabled || isLoading) return;
|
||||
if (isDisabled || isLoading) return;
|
||||
|
||||
if (!open) {
|
||||
setIsPageSelectionOpen(false);
|
||||
|
|
@ -122,7 +127,7 @@ function NewActionButton(props: NewActionButtonProps) {
|
|||
|
||||
setIsPageSelectionOpen(true);
|
||||
},
|
||||
[pages, createQueryAction, disabled, isLoading],
|
||||
[pages, createQueryAction, isDisabled, isLoading],
|
||||
);
|
||||
|
||||
const getCreateButtonText = () => {
|
||||
|
|
@ -139,11 +144,11 @@ function NewActionButton(props: NewActionButtonProps) {
|
|||
|
||||
return (
|
||||
<Menu onOpenChange={handleOnInteraction} open={isPageSelectionOpen}>
|
||||
<MenuTrigger disabled={disabled}>
|
||||
<MenuTrigger disabled={isDisabled}>
|
||||
<Button
|
||||
className="t--create-query"
|
||||
id={"create-query"}
|
||||
isDisabled={!!disabled}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isSelected || isLoading}
|
||||
kind={isNewQuerySecondaryButton ? "secondary" : "primary"}
|
||||
onClick={() => handleOnInteraction(true)}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import LeftSideContent from "./LeftSideContent";
|
|||
import { getAppsmithConfigs } from "ee/configs";
|
||||
import { useIsMobileDevice } from "utils/hooks/useDeviceDetect";
|
||||
import styled from "styled-components";
|
||||
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface ContainerProps {
|
||||
title: string;
|
||||
|
|
@ -43,10 +45,15 @@ function Container(props: ContainerProps) {
|
|||
const organizationConfig = useSelector(getOrganizationConfig);
|
||||
const { cloudHosting } = getAppsmithConfigs();
|
||||
const isMobileDevice = useIsMobileDevice();
|
||||
const isAiAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
||||
|
||||
return (
|
||||
<ContainerWrapper
|
||||
className={`gap-14 my-auto flex items-center justify-center min-w-min`}
|
||||
className={clsx({
|
||||
"my-auto flex items-center justify-center min-w-min": true,
|
||||
"flex-col-reverse gap-4": isAiAgentFlowEnabled,
|
||||
"flex-row gap-14": !isAiAgentFlowEnabled,
|
||||
})}
|
||||
data-testid={testId}
|
||||
>
|
||||
{cloudHosting && !isMobileDevice && <LeftSideContent />}
|
||||
|
|
|
|||
|
|
@ -71,33 +71,30 @@ const QUOTE = {
|
|||
authorImage: `${getAssetUrl(`${ASSETS_CDN_URL}/thomas-zwick.png`)}`,
|
||||
};
|
||||
|
||||
const QUOTE_FOR_AGENTS = {
|
||||
quote: `Our goal was to have an omni-channel AI system that could help our usersin every step of the journey. Appsmith serves as a command center for us to control the behavior of the agent. This is a competitive advantage. We're able to serve our customers much faster than our competitors`,
|
||||
author: "Shawn Lim",
|
||||
authorTitle: "VP, Platform & AI, Funding Societies",
|
||||
authorImage: "https://assets.appsmith.com/fundingsocieties-logo.svg",
|
||||
};
|
||||
|
||||
function LeftSideContent() {
|
||||
const isAiAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
||||
|
||||
const quote = isAiAgentFlowEnabled ? QUOTE_FOR_AGENTS : QUOTE;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className="left-description">
|
||||
<div className="left-description-container">
|
||||
"{quote.quote}"
|
||||
{!isAiAgentFlowEnabled && (
|
||||
<div className="left-description">
|
||||
<div className="left-description-container">
|
||||
"{QUOTE.quote}"
|
||||
</div>
|
||||
<div className="left-description-author">
|
||||
{QUOTE.authorImage && (
|
||||
<Avatar
|
||||
image={QUOTE.authorImage}
|
||||
label={QUOTE.author}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
<div>{QUOTE.author}</div>
|
||||
<div className="dot">·</div>
|
||||
<div>{QUOTE.authorTitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="left-description-author">
|
||||
{quote.authorImage && (
|
||||
<Avatar image={quote.authorImage} label={quote.author} size="sm" />
|
||||
)}
|
||||
<div>{quote.author}</div>
|
||||
<div className="dot">·</div>
|
||||
<div>{quote.authorTitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="client-logo-container">
|
||||
<div className="client-heading">
|
||||
|
|
|
|||
|
|
@ -168,6 +168,10 @@ export function isGoogleSheetPluginDS(pluginPackageName?: string) {
|
|||
return pluginPackageName === PluginPackageName.GOOGLE_SHEETS;
|
||||
}
|
||||
|
||||
export function isMongoDBPluginDS(pluginPackageName?: string) {
|
||||
return pluginPackageName === PluginPackageName.MONGO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns datasource property value from datasource?.datasourceConfiguration?.properties
|
||||
* @param datasource Datasource
|
||||
|
|
|
|||
|
|
@ -213,4 +213,24 @@ public class WidgetRefactorUtil {
|
|||
throw new AppsmithException(AppsmithError.JSON_PROCESSING_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> extractWidgetNamesFromDsl(JsonNode dsl) {
|
||||
Set<String> widgetNames = new HashSet<>();
|
||||
extractWidgetNamesRecursive(dsl, widgetNames);
|
||||
return widgetNames;
|
||||
}
|
||||
|
||||
private void extractWidgetNamesRecursive(JsonNode dsl, Set<String> widgetNames) {
|
||||
if (dsl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dsl.has(FieldName.WIDGET_NAME)) {
|
||||
widgetNames.add(dsl.get(FieldName.WIDGET_NAME).asText());
|
||||
}
|
||||
|
||||
if (dsl.has("children")) {
|
||||
dsl.get("children").forEach(child -> extractWidgetNamesRecursive(child, widgetNames));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user