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
|
app/client/src/ee/JSFunctionExecutionSaga.ts @ApekshaBhosale
|
||||||
|
|
||||||
# Enterprise Success
|
# 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
|
# DevOps
|
||||||
deploy/**/* @sharat87 @pratapaprasanna
|
deploy/**/* @sharat87 @pratapaprasanna @nidhi-nair
|
||||||
.github/workflows/*.yml @sharat87
|
.github/workflows/*.yml @sharat87 @nidhi-nair
|
||||||
app/client/packages/ctl/**/* @sharat87 @pratapaprasanna
|
app/client/packages/ctl/**/* @sharat87 @pratapaprasanna @nidhi-nair
|
||||||
app/server/**/pom.xml @sharat87
|
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/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/repositories/ce/params/QueryAllParams.java @sharat87
|
||||||
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/bridge/**/* @sharat87
|
app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/bridge/**/* @sharat87
|
||||||
|
|
||||||
#Cypress
|
# Cypress
|
||||||
app/client/cypress/**/* @ApekshaBhosale @sagar-qa007
|
app/client/cypress/**/* @ApekshaBhosale
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export const isEllipsisActive = (element: HTMLElement | null) => {
|
||||||
export const EditableEntityName = (props: EditableEntityNameProps) => {
|
export const EditableEntityName = (props: EditableEntityNameProps) => {
|
||||||
const {
|
const {
|
||||||
canEdit,
|
canEdit,
|
||||||
|
hasError,
|
||||||
icon,
|
icon,
|
||||||
inputTestId,
|
inputTestId,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
|
@ -106,6 +107,7 @@ export const EditableEntityName = (props: EditableEntityNameProps) => {
|
||||||
<Styled.Text
|
<Styled.Text
|
||||||
aria-invalid={Boolean(validationError)}
|
aria-invalid={Boolean(validationError)}
|
||||||
className={clsx("t--entity-name", { editing: inEditMode })}
|
className={clsx("t--entity-name", { editing: inEditMode })}
|
||||||
|
color={hasError ? "var(--ads-v2-color-fg-error)" : undefined}
|
||||||
data-isediting={inEditMode}
|
data-isediting={inEditMode}
|
||||||
data-isfixedwidth={isFixedWidth}
|
data-isfixedwidth={isFixedWidth}
|
||||||
inputProps={inputProps}
|
inputProps={inputProps}
|
||||||
|
|
|
||||||
|
|
@ -27,4 +27,6 @@ export interface EditableEntityNameProps {
|
||||||
normalizeName?: boolean;
|
normalizeName?: boolean;
|
||||||
/** Used for showing ellipsis for longer names */
|
/** Used for showing ellipsis for longer names */
|
||||||
showEllipsis?: boolean;
|
showEllipsis?: boolean;
|
||||||
|
/** Whether to show the entity is in error state */
|
||||||
|
hasError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export const EntityItem = (props: EntityItemProps) => {
|
||||||
return (
|
return (
|
||||||
<EditableEntityName
|
<EditableEntityName
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
|
hasError={props.hasError}
|
||||||
icon={startIcon}
|
icon={startIcon}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
isFixedWidth
|
isFixedWidth
|
||||||
|
|
@ -50,6 +51,7 @@ export const EntityItem = (props: EntityItemProps) => {
|
||||||
normalizeName,
|
normalizeName,
|
||||||
onEditComplete,
|
onEditComplete,
|
||||||
onNameSave,
|
onNameSave,
|
||||||
|
props.hasError,
|
||||||
props.title,
|
props.title,
|
||||||
startIcon,
|
startIcon,
|
||||||
validateName,
|
validateName,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export interface EntityListTreeItem {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
hasError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityListTreeProps {
|
export interface EntityListTreeProps {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||||
import { Text } from "@appsmith/ads";
|
import { Text } from "@appsmith/ads";
|
||||||
import { useIsEditorInitialised } from "IDE/hooks";
|
import { useIsEditorInitialised } from "IDE/hooks";
|
||||||
import { useActionSettingsConfig } from "./hooks";
|
import { useActionSettingsConfig } from "./hooks";
|
||||||
|
import { createMessage, PLUGIN_NOT_INSTALLED } from "ee/constants/messages";
|
||||||
|
import { ShowUpgradeMenuItem } from "ee/utils/licenseHelpers";
|
||||||
|
|
||||||
interface ChildrenProps {
|
interface ChildrenProps {
|
||||||
children: React.ReactNode | React.ReactNode[];
|
children: React.ReactNode | React.ReactNode[];
|
||||||
|
|
@ -54,10 +56,11 @@ const PluginActionEditor = (props: ChildrenProps) => {
|
||||||
|
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
return (
|
return (
|
||||||
<CenteredWrapper>
|
<CenteredWrapper className="flex-col">
|
||||||
<Text color="var(--ads-v2-color-fg-error)" kind="heading-m">
|
<Text color="var(--ads-v2-color-fg-error)" kind="heading-m">
|
||||||
Plugin not installed!
|
{createMessage(PLUGIN_NOT_INSTALLED)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<ShowUpgradeMenuItem />
|
||||||
</CenteredWrapper>
|
</CenteredWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { DatasourceConnectionMode } from "entities/Datasource";
|
import { DatasourceConnectionMode } from "entities/Datasource";
|
||||||
import Snowflake from ".";
|
import Snowflake from ".";
|
||||||
|
import { SSLType } from "entities/Datasource/RestAPIForm";
|
||||||
|
|
||||||
describe("Snowflake WidgetQueryGenerator", () => {
|
describe("Snowflake WidgetQueryGenerator", () => {
|
||||||
const initialValues = {
|
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 { formatDialect, snowflake } from "sql-formatter";
|
||||||
import { QUERY_TYPE } from "../types";
|
import { removeSpecialChars } from "utils/helpers";
|
||||||
|
import { BaseQueryGenerator } from "../BaseQueryGenerator";
|
||||||
import type {
|
import type {
|
||||||
|
ActionConfigurationSQL,
|
||||||
WidgetQueryGenerationConfig,
|
WidgetQueryGenerationConfig,
|
||||||
WidgetQueryGenerationFormConfig,
|
WidgetQueryGenerationFormConfig,
|
||||||
ActionConfigurationSQL,
|
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { removeSpecialChars } from "utils/helpers";
|
import { QUERY_TYPE } from "../types";
|
||||||
import { without } from "lodash";
|
|
||||||
import { DatasourceConnectionMode } from "entities/Datasource";
|
|
||||||
|
|
||||||
export default abstract class Snowflake extends BaseQueryGenerator {
|
export default abstract class Snowflake extends BaseQueryGenerator {
|
||||||
private static buildSelect(
|
private static buildSelect(
|
||||||
|
|
@ -249,4 +252,13 @@ export default abstract class Snowflake extends BaseQueryGenerator {
|
||||||
static getTotalRecordExpression(binding: string) {
|
static getTotalRecordExpression(binding: string) {
|
||||||
return `${binding}[0].count`;
|
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",
|
"Make your app public to embed your Appsmith app into legacy applications",
|
||||||
secondaryHeading: () =>
|
secondaryHeading: () =>
|
||||||
"Embedding in public mode is supported in the free plan. To make your app public, please contact your administrator.",
|
"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: () =>
|
chromeExtensionBannerDescription: () =>
|
||||||
"Bring powerful AI assistance to the tools you and your teams use.",
|
"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 = {
|
export const APP_NAVIGATION_SETTING = {
|
||||||
|
|
@ -2691,3 +2693,6 @@ export const GOOGLE_RECAPTCHA_FAILED = () =>
|
||||||
"Google reCAPTCHA verification failed";
|
"Google reCAPTCHA verification failed";
|
||||||
export const PASSWORD_INSUFFICIENT_STRENGTH = () =>
|
export const PASSWORD_INSUFFICIENT_STRENGTH = () =>
|
||||||
"Insufficient password 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[]) => {
|
const getListItems = (data: ActionOperation[]) => {
|
||||||
return data.map((fileOperation) => {
|
return data
|
||||||
let title =
|
.map((fileOperation) => {
|
||||||
fileOperation.entityExplorerTitle ||
|
let title =
|
||||||
fileOperation.dsName ||
|
fileOperation.entityExplorerTitle ||
|
||||||
fileOperation.title;
|
fileOperation.dsName ||
|
||||||
|
fileOperation.title;
|
||||||
|
|
||||||
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:
|
|
||||||
fileOperation.focusEntityType === FocusEntity.QUERY_MODULE_INSTANCE
|
fileOperation.focusEntityType === FocusEntity.QUERY_MODULE_INSTANCE
|
||||||
? fileOperation.dsName
|
? fileOperation.title
|
||||||
: "",
|
: title;
|
||||||
descriptionType: "inline",
|
const className = createAddClassName(title);
|
||||||
onClick: onCreateItemClick.bind(null, fileOperation),
|
const icon =
|
||||||
} as ListItemProps;
|
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 };
|
return { getListItems };
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,9 @@ export const useFilteredFileOperations = ({
|
||||||
}: FilterFileOperationsProps) => {
|
}: FilterFileOperationsProps) => {
|
||||||
const { appWideDS = [], otherDS = [] } = useAppWideAndOtherDatasource();
|
const { appWideDS = [], otherDS = [] } = useAppWideAndOtherDatasource();
|
||||||
const plugins = useSelector(getPlugins);
|
const plugins = useSelector(getPlugins);
|
||||||
|
const pluginById = useMemo(() => {
|
||||||
|
return keyBy(plugins, "id");
|
||||||
|
}, [plugins]);
|
||||||
const moduleOptions = useModuleOptions();
|
const moduleOptions = useModuleOptions();
|
||||||
const workflowOptions = useWorkflowOptions();
|
const workflowOptions = useWorkflowOptions();
|
||||||
|
|
||||||
|
|
@ -106,7 +109,7 @@ export const useFilteredFileOperations = ({
|
||||||
return AiPlugin.id !== ds.pluginId;
|
return AiPlugin.id !== ds.pluginId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return !!pluginById[ds.pluginId]?.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return useFilteredAndSortedFileOperations({
|
return useFilteredAndSortedFileOperations({
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ import type { ExplorerURLParams } from "ee/pages/Editor/Explorer/helpers";
|
||||||
import { getLastSelectedWidget } from "selectors/ui";
|
import { getLastSelectedWidget } from "selectors/ui";
|
||||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||||
import useRecentEntities from "./useRecentEntities";
|
import useRecentEntities from "./useRecentEntities";
|
||||||
import { noop } from "lodash";
|
import { keyBy, noop } from "lodash";
|
||||||
import {
|
import {
|
||||||
getCurrentPageId,
|
getCurrentPageId,
|
||||||
getPagePermissions,
|
getPagePermissions,
|
||||||
|
|
@ -179,6 +179,9 @@ function GlobalSearch() {
|
||||||
(state: AppState) => state.ui.globalSearch.filterContext.category,
|
(state: AppState) => state.ui.globalSearch.filterContext.category,
|
||||||
);
|
);
|
||||||
const plugins = useSelector(getPlugins);
|
const plugins = useSelector(getPlugins);
|
||||||
|
const pluginById = useMemo(() => {
|
||||||
|
return keyBy(plugins, "id");
|
||||||
|
}, [plugins]);
|
||||||
const setCategory = useCallback(
|
const setCategory = useCallback(
|
||||||
(category: SearchCategory) => {
|
(category: SearchCategory) => {
|
||||||
dispatch(setGlobalSearchFilterContext({ category: category }));
|
dispatch(setGlobalSearchFilterContext({ category: category }));
|
||||||
|
|
@ -233,10 +236,15 @@ function GlobalSearch() {
|
||||||
}, [basePageIdToPageIdMap, params?.basePageId, reducerDatasources]);
|
}, [basePageIdToPageIdMap, params?.basePageId, reducerDatasources]);
|
||||||
|
|
||||||
const filteredDatasources = useMemo(() => {
|
const filteredDatasources = useMemo(() => {
|
||||||
if (!query) return datasourcesList;
|
if (!query)
|
||||||
|
return datasourcesList.filter(
|
||||||
|
(datasource) => pluginById[datasource.pluginId]?.id,
|
||||||
|
);
|
||||||
|
|
||||||
return datasourcesList.filter((datasource) =>
|
return datasourcesList.filter(
|
||||||
isMatching(datasource.name, query),
|
(datasource) =>
|
||||||
|
isMatching(datasource.name, query) &&
|
||||||
|
pluginById[datasource.pluginId]?.id,
|
||||||
);
|
);
|
||||||
}, [datasourcesList, query]);
|
}, [datasourcesList, query]);
|
||||||
const recentEntities = useRecentEntities();
|
const recentEntities = useRecentEntities();
|
||||||
|
|
|
||||||
|
|
@ -22,27 +22,42 @@ const IconContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Label = styled.div`
|
const LabelContainer = styled.div`
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Label = styled.div`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const SubText = styled.span`
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ads-v2-color-fg-muted);
|
||||||
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
className?: string;
|
||||||
label?: JSX.Element | string;
|
label?: JSX.Element | string;
|
||||||
leftIcon?: JSX.Element;
|
leftIcon?: JSX.Element;
|
||||||
rightIcon?: JSX.Element;
|
rightIcon?: JSX.Element;
|
||||||
className?: string;
|
subText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownOption(props: Props) {
|
export function DropdownOption(props: Props) {
|
||||||
const { className, label, leftIcon, rightIcon } = props;
|
const { className, label, leftIcon, rightIcon, subText } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={className}>
|
<Container className={className}>
|
||||||
<LeftSection>
|
<LeftSection>
|
||||||
{leftIcon && <IconContainer>{leftIcon}</IconContainer>}
|
{leftIcon && <IconContainer>{leftIcon}</IconContainer>}
|
||||||
<Label>{label}</Label>
|
<LabelContainer>
|
||||||
|
<Label>{label}</Label>
|
||||||
|
{subText && <SubText>{subText}</SubText>}
|
||||||
|
</LabelContainer>
|
||||||
</LeftSection>
|
</LeftSection>
|
||||||
{rightIcon && <IconContainer>{rightIcon}</IconContainer>}
|
{rightIcon && <IconContainer>{rightIcon}</IconContainer>}
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
import React, { memo, useContext } from "react";
|
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 { useTableOrSpreadsheet } from "./useTableOrSpreadsheet";
|
||||||
import { Select, Option, Tooltip } from "@appsmith/ads";
|
import { Select, Option, Tooltip } from "@appsmith/ads";
|
||||||
import { DropdownOption } from "../DatasourceDropdown/DropdownOption";
|
import { DropdownOption } from "../DatasourceDropdown/DropdownOption";
|
||||||
import type { DefaultOptionType } from "rc-select/lib/Select";
|
import type { DefaultOptionType } from "rc-select/lib/Select";
|
||||||
import { ColumnSelectorModal } from "../ColumnSelectorModal";
|
import { ColumnSelectorModal } from "../ColumnSelectorModal";
|
||||||
import { WidgetQueryGeneratorFormContext } from "components/editorComponents/WidgetQueryGeneratorForm/index";
|
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() {
|
function TableOrSpreadsheetDropdown() {
|
||||||
const {
|
const {
|
||||||
|
|
@ -55,10 +63,18 @@ function TableOrSpreadsheetDropdown() {
|
||||||
return (
|
return (
|
||||||
<Option
|
<Option
|
||||||
data-testid="t--one-click-binding-table-selector--table"
|
data-testid="t--one-click-binding-table-selector--table"
|
||||||
|
disabled={option.disabled}
|
||||||
key={option.id}
|
key={option.id}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
>
|
>
|
||||||
<DropdownOption label={option.label} />
|
<DropdownOption
|
||||||
|
label={option.label}
|
||||||
|
subText={
|
||||||
|
option.disabled
|
||||||
|
? createMessage(NO_PRIMARY_KEYS_MESSAGE)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Option>
|
</Option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -66,6 +82,10 @@ function TableOrSpreadsheetDropdown() {
|
||||||
<ErrorMessage data-testid="t--one-click-binding-table-selector--error">
|
<ErrorMessage data-testid="t--one-click-binding-table-selector--error">
|
||||||
{error}
|
{error}
|
||||||
</ErrorMessage>
|
</ErrorMessage>
|
||||||
|
|
||||||
|
<PrimaryKeysMessage>
|
||||||
|
{createMessage(PRIMARY_KEYS_MESSAGE)}
|
||||||
|
</PrimaryKeysMessage>
|
||||||
</SelectWrapper>
|
</SelectWrapper>
|
||||||
);
|
);
|
||||||
} else {
|
} 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 { fetchGheetSheets } from "actions/datasourceActions";
|
||||||
import { useCallback, useContext, useMemo } from "react";
|
import type { AppState } from "ee/reducers";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import {
|
import {
|
||||||
getDatasource,
|
getDatasource,
|
||||||
getDatasourceLoading,
|
getDatasourceLoading,
|
||||||
|
|
@ -9,21 +7,25 @@ import {
|
||||||
getIsFetchingDatasourceStructure,
|
getIsFetchingDatasourceStructure,
|
||||||
getPluginPackageFromDatasourceId,
|
getPluginPackageFromDatasourceId,
|
||||||
} from "ee/selectors/entitiesSelector";
|
} from "ee/selectors/entitiesSelector";
|
||||||
import { WidgetQueryGeneratorFormContext } from "../..";
|
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||||
import { Bold, Label } from "../../styles";
|
import type { DatasourceStructure, DatasourceTable } from "entities/Datasource";
|
||||||
import { PluginFormInputFieldMap } from "../../constants";
|
import React, { useCallback, useContext, useMemo } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { getWidget } from "sagas/selectors";
|
||||||
import {
|
import {
|
||||||
getGsheetSpreadsheets,
|
getGsheetSpreadsheets,
|
||||||
getIsFetchingGsheetSpreadsheets,
|
getIsFetchingGsheetSpreadsheets,
|
||||||
} from "selectors/datasourceSelectors";
|
} 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 { getisOneClickBindingConnectingForWidget } from "selectors/oneClickBindingSelectors";
|
||||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
import {
|
||||||
import { getWidget } from "sagas/selectors";
|
isGoogleSheetPluginDS,
|
||||||
import type { DatasourceStructure } from "entities/Datasource";
|
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() {
|
export function useTableOrSpreadsheet() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
@ -63,6 +65,10 @@ export function useTableOrSpreadsheet() {
|
||||||
).TABLE
|
).TABLE
|
||||||
: "table";
|
: "table";
|
||||||
|
|
||||||
|
const tableHasPrimaryKeys = (table: DatasourceTable) => {
|
||||||
|
return table.keys && table.keys.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
isGoogleSheetPluginDS(selectedDatasourcePluginPackageName) &&
|
isGoogleSheetPluginDS(selectedDatasourcePluginPackageName) &&
|
||||||
|
|
@ -72,19 +78,35 @@ export function useTableOrSpreadsheet() {
|
||||||
id: value,
|
id: value,
|
||||||
label: label,
|
label: label,
|
||||||
value: label,
|
value: label,
|
||||||
|
disabled: false,
|
||||||
data: {
|
data: {
|
||||||
tableName: value,
|
tableName: value,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else if (datasourceStructure) {
|
} else if (isMongoDBPluginDS(selectedDatasourcePluginPackageName)) {
|
||||||
return (datasourceStructure.tables || []).map(({ name }) => ({
|
return (datasourceStructure.tables || []).map((table) => ({
|
||||||
id: name,
|
id: table.name,
|
||||||
label: name,
|
label: table.name,
|
||||||
value: name,
|
value: table.name,
|
||||||
data: {
|
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 {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,8 @@ export const PluginFormInputFieldMap: Record<
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_QUERY_OPTIONS_COUNTS_TO_SHOW = 4;
|
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;
|
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`
|
export const Placeholder = styled.div`
|
||||||
color: var(--ads-v2-color-fg-subtle);
|
color: var(--ads-v2-color-fg-subtle);
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ describe("ComputeTablePropertyControlV2.getInputComputedValue", () => {
|
||||||
const tableName = "Table1";
|
const tableName = "Table1";
|
||||||
const inputVariations = [
|
const inputVariations = [
|
||||||
"currentRow.price",
|
"currentRow.price",
|
||||||
|
`JSObject1.somefunction(currentRow["id"] || 0)`,
|
||||||
`
|
`
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -58,4 +59,45 @@ describe("ComputeTablePropertyControlV2.getInputComputedValue", () => {
|
||||||
ComputeTablePropertyControlV2.getInputComputedValue(computedValue),
|
ComputeTablePropertyControlV2.getInputComputedValue(computedValue),
|
||||||
).toBe(`{{currentRow.quantity}}{{5}}`);
|
).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;
|
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
|
// 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 })()}}"
|
// 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);
|
const mapSignatureIndex = propertyValue.indexOf(MAP_FUNCTION_SIGNATURE);
|
||||||
|
|
||||||
// Find the actual computation expression between the map parentheses
|
// Find the actual computation expression between the map parentheses
|
||||||
const computationStart = mapSignatureIndex + MAP_FUNCTION_SIGNATURE.length;
|
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
|
// 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(
|
const computationExpression = propertyValue.substring(
|
||||||
computationStart,
|
computationStart,
|
||||||
computationEnd,
|
endIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
return JSToString(computationExpression);
|
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) => {
|
getComputedValue = (value: string, tableName: string) => {
|
||||||
// Return raw value if:
|
// Return raw value if:
|
||||||
// 1. The value is not a dynamic binding (not wrapped in {{...}})
|
// 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 { useLocation } from "react-router";
|
||||||
import { useHeaderActions } from "ee/hooks/datasourceEditorHooks";
|
import { useHeaderActions } from "ee/hooks/datasourceEditorHooks";
|
||||||
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
|
import { getIDETypeByUrl } from "ee/entities/IDE/utils";
|
||||||
|
import { getPlugin } from "ee/selectors/entitiesSelector";
|
||||||
|
|
||||||
export const ActionWrapper = styled.div`
|
export const ActionWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -118,6 +119,10 @@ export const DSFormHeader = (props: DSFormHeaderProps) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const ideType = getIDETypeByUrl(location.pathname);
|
const ideType = getIDETypeByUrl(location.pathname);
|
||||||
|
const plugin = useSelector((state) =>
|
||||||
|
getPlugin(state, datasource?.pluginId || ""),
|
||||||
|
);
|
||||||
|
const isEditDisabled = !plugin?.id;
|
||||||
|
|
||||||
const deleteAction = () => {
|
const deleteAction = () => {
|
||||||
if (isDeleting) return;
|
if (isDeleting) return;
|
||||||
|
|
@ -210,8 +215,11 @@ export const DSFormHeader = (props: DSFormHeaderProps) => {
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="t--edit-datasource"
|
className="t--edit-datasource"
|
||||||
|
isDisabled={isEditDisabled}
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (isEditDisabled) return;
|
||||||
|
|
||||||
setDatasourceViewMode({
|
setDatasourceViewMode({
|
||||||
datasourceId: datasourceId,
|
datasourceId: datasourceId,
|
||||||
viewMode: false,
|
viewMode: false,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { getCurrentPageId, getPageList } from "selectors/editorSelectors";
|
||||||
import type { Datasource } from "entities/Datasource";
|
import type { Datasource } from "entities/Datasource";
|
||||||
import type { EventLocation } from "ee/utils/analyticsUtilTypes";
|
import type { EventLocation } from "ee/utils/analyticsUtilTypes";
|
||||||
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
|
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
|
||||||
import { getSelectedTableName } from "ee/selectors/entitiesSelector";
|
import { getPlugin, getSelectedTableName } from "ee/selectors/entitiesSelector";
|
||||||
|
|
||||||
interface NewActionButtonProps {
|
interface NewActionButtonProps {
|
||||||
datasource?: Datasource;
|
datasource?: Datasource;
|
||||||
|
|
@ -75,6 +75,11 @@ function NewActionButton(props: NewActionButtonProps) {
|
||||||
...pages.filter((p) => p.pageId !== currentPageId),
|
...pages.filter((p) => p.pageId !== currentPageId),
|
||||||
];
|
];
|
||||||
const queryDefaultTableName = useSelector(getSelectedTableName);
|
const queryDefaultTableName = useSelector(getSelectedTableName);
|
||||||
|
const plugin = useSelector((state) =>
|
||||||
|
getPlugin(state, datasource?.pluginId || ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDisabled = !!disabled || !plugin?.id;
|
||||||
|
|
||||||
const createQueryAction = useCallback(
|
const createQueryAction = useCallback(
|
||||||
(pageId: string) => {
|
(pageId: string) => {
|
||||||
|
|
@ -106,7 +111,7 @@ function NewActionButton(props: NewActionButtonProps) {
|
||||||
|
|
||||||
const handleOnInteraction = useCallback(
|
const handleOnInteraction = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
if (disabled || isLoading) return;
|
if (isDisabled || isLoading) return;
|
||||||
|
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setIsPageSelectionOpen(false);
|
setIsPageSelectionOpen(false);
|
||||||
|
|
@ -122,7 +127,7 @@ function NewActionButton(props: NewActionButtonProps) {
|
||||||
|
|
||||||
setIsPageSelectionOpen(true);
|
setIsPageSelectionOpen(true);
|
||||||
},
|
},
|
||||||
[pages, createQueryAction, disabled, isLoading],
|
[pages, createQueryAction, isDisabled, isLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCreateButtonText = () => {
|
const getCreateButtonText = () => {
|
||||||
|
|
@ -139,11 +144,11 @@ function NewActionButton(props: NewActionButtonProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu onOpenChange={handleOnInteraction} open={isPageSelectionOpen}>
|
<Menu onOpenChange={handleOnInteraction} open={isPageSelectionOpen}>
|
||||||
<MenuTrigger disabled={disabled}>
|
<MenuTrigger disabled={isDisabled}>
|
||||||
<Button
|
<Button
|
||||||
className="t--create-query"
|
className="t--create-query"
|
||||||
id={"create-query"}
|
id={"create-query"}
|
||||||
isDisabled={!!disabled}
|
isDisabled={isDisabled}
|
||||||
isLoading={isSelected || isLoading}
|
isLoading={isSelected || isLoading}
|
||||||
kind={isNewQuerySecondaryButton ? "secondary" : "primary"}
|
kind={isNewQuerySecondaryButton ? "secondary" : "primary"}
|
||||||
onClick={() => handleOnInteraction(true)}
|
onClick={() => handleOnInteraction(true)}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import LeftSideContent from "./LeftSideContent";
|
||||||
import { getAppsmithConfigs } from "ee/configs";
|
import { getAppsmithConfigs } from "ee/configs";
|
||||||
import { useIsMobileDevice } from "utils/hooks/useDeviceDetect";
|
import { useIsMobileDevice } from "utils/hooks/useDeviceDetect";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
interface ContainerProps {
|
interface ContainerProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -43,10 +45,15 @@ function Container(props: ContainerProps) {
|
||||||
const organizationConfig = useSelector(getOrganizationConfig);
|
const organizationConfig = useSelector(getOrganizationConfig);
|
||||||
const { cloudHosting } = getAppsmithConfigs();
|
const { cloudHosting } = getAppsmithConfigs();
|
||||||
const isMobileDevice = useIsMobileDevice();
|
const isMobileDevice = useIsMobileDevice();
|
||||||
|
const isAiAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContainerWrapper
|
<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}
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
{cloudHosting && !isMobileDevice && <LeftSideContent />}
|
{cloudHosting && !isMobileDevice && <LeftSideContent />}
|
||||||
|
|
|
||||||
|
|
@ -71,33 +71,30 @@ const QUOTE = {
|
||||||
authorImage: `${getAssetUrl(`${ASSETS_CDN_URL}/thomas-zwick.png`)}`,
|
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() {
|
function LeftSideContent() {
|
||||||
const isAiAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
const isAiAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
||||||
|
|
||||||
const quote = isAiAgentFlowEnabled ? QUOTE_FOR_AGENTS : QUOTE;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<div className="left-description">
|
{!isAiAgentFlowEnabled && (
|
||||||
<div className="left-description-container">
|
<div className="left-description">
|
||||||
"{quote.quote}"
|
<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>
|
||||||
<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-logo-container">
|
||||||
<div className="client-heading">
|
<div className="client-heading">
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,10 @@ export function isGoogleSheetPluginDS(pluginPackageName?: string) {
|
||||||
return pluginPackageName === PluginPackageName.GOOGLE_SHEETS;
|
return pluginPackageName === PluginPackageName.GOOGLE_SHEETS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMongoDBPluginDS(pluginPackageName?: string) {
|
||||||
|
return pluginPackageName === PluginPackageName.MONGO;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns datasource property value from datasource?.datasourceConfiguration?.properties
|
* Returns datasource property value from datasource?.datasourceConfiguration?.properties
|
||||||
* @param datasource Datasource
|
* @param datasource Datasource
|
||||||
|
|
|
||||||
|
|
@ -213,4 +213,24 @@ public class WidgetRefactorUtil {
|
||||||
throw new AppsmithException(AppsmithError.JSON_PROCESSING_ERROR);
|
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