Merge pull request #31476 from appsmithorg/release

05/03 Daily promotion
This commit is contained in:
yatinappsmith 2024-03-05 11:50:48 +05:30 committed by GitHub
commit db4f473701
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
107 changed files with 1566 additions and 265 deletions

2
.github/config.json vendored

File diff suppressed because one or more lines are too long

View File

@ -182,17 +182,15 @@ jobs:
fi
if ! mvn test "${args[@]}"; then
echo "Generating failed test list:"
failed_tests_file="$PWD/failed-server-tests.txt"
gawk -F\" '/<testcase / {cur_test = $4 "#" $2} /<failure / {print cur_test}' $(find . -type f -name 'TEST-*.xml') \
| tee failed-server-tests.txt
echo "Report files found:"
find . -type f -name 'TEST-*.xml'
echo "Test case failure lines:"
grep -FoB1 '<failure ' $(find . -type f -name 'TEST-*.xml') || true
if [[ -s failed-server-tests.txt ]]; then
| tee "$failed_tests_file"
echo "Saved to $failed_tests_file"
if [[ -s $failed_tests_file ]]; then
content="$(
echo "## Failed server tests"
echo
sed 's/^/- /' 'failed-server-tests.txt' | sort -u
sed 's/^/- /' "$failed_tests_file" | sort -u
)"
echo "$content" >> "$GITHUB_STEP_SUMMARY"
# Add a comment to the PR with the list of failed tests.

View File

@ -17,6 +17,9 @@ module.exports = {
babel: {
plugins: ["babel-plugin-lodash"],
},
eslint: {
enable: false,
},
webpack: {
configure: {
resolve: {

View File

@ -7,11 +7,18 @@ module.exports = merge(common, {
client: {
overlay: {
warnings: false,
errors: false
}
}
errors: false,
},
},
},
optimization: {
minimize: false,
},
cache: {
type: "filesystem",
memoryCacheUnaffected: true,
},
experiments: {
cacheUnaffected: true,
},
});

View File

@ -17,7 +17,7 @@
],
"scripts": {
"analyze": "yarn cra-bundle-analyzer",
"start": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT REACT_APP_CLIENT_LOG_LEVEL=debug HOST=dev.appsmith.com craco --max_old_space_size=7168 start",
"start": "BROWSER=none REACT_APP_ENVIRONMENT=DEVELOPMENT REACT_APP_CLIENT_LOG_LEVEL=debug HOST=dev.appsmith.com craco --max_old_space_size=4096 start",
"build": "./build.sh",
"build-airgap": "node download-assets.js && ./build.sh",
"build-local": "craco --max-old-space-size=4096 build --config craco.build.config.js",

View File

@ -6,13 +6,13 @@ import { Item, Menu, MenuList } from "../../Menu";
import { useListState } from "@react-stately/list";
import styles from "./styles.module.css";
import type { ActionGroupProps } from "./types";
import type { ButtonGroupProps } from "../../../index";
import { useActionGroup } from "./useActionGroup";
import { IconButton } from "../../IconButton";
import { ActionGroupItem } from "./ActionGroupItem";
const _ActionGroup = <T extends object>(
props: ActionGroupProps<T>,
props: ButtonGroupProps<T>,
ref: DOMRef<HTMLDivElement>,
) => {
const {
@ -52,7 +52,7 @@ const _ActionGroup = <T extends object>(
>
<div
className={styles.actionGroup}
data-density={density ? density : undefined}
data-density={Boolean(density) ? density : undefined}
data-orientation={orientation}
data-overflow={overflowMode}
ref={domRef}
@ -60,8 +60,9 @@ const _ActionGroup = <T extends object>(
{...others}
>
{children.map((item) => {
if (Boolean(item.props.isSeparator))
return <div data-separator="" />;
if (Boolean(item.props.isSeparator)) {
return <div data-separator="" key={item.key} />;
}
return (
<ActionGroupItem

View File

@ -1,11 +1,11 @@
import React, { forwardRef } from "react";
import { Button } from "@design-system/widgets";
import type { ActionGroupItemProps } from "./types";
import type { ButtonGroupItemProps } from "../../../";
import type { ButtonRef as HeadlessButtonRef } from "@design-system/headless";
const _ActionGroupItem = <T extends object>(
props: ActionGroupItemProps<T>,
props: ButtonGroupItemProps<T>,
ref: HeadlessButtonRef,
) => {
const { color, item, variant, ...rest } = props;

View File

@ -1,3 +1,2 @@
export * from "./types";
export { ActionGroup } from "./ActionGroup";
export { ActionGroupItem } from "./ActionGroupItem";

View File

@ -3,7 +3,7 @@ import type { ListState } from "@react-stately/list";
import { useCallback, type RefObject, useMemo } from "react";
import type { DOMAttributes, FocusableElement } from "@react-types/shared";
import type { ActionGroupProps } from "./types";
import type { ButtonGroupProps } from "../../../";
import {
useLayoutEffect,
useResizeObserver,
@ -17,7 +17,7 @@ export interface ActionGroupAria {
}
export function useActionGroup<T>(
props: ActionGroupProps<T>,
props: ButtonGroupProps<T>,
state: ListState<T>,
ref: RefObject<FocusableElement>,
): ActionGroupAria {

View File

@ -14,6 +14,7 @@
padding-inline: var(--inner-spacing-3);
padding-block: var(--inner-spacing-2);
min-inline-size: var(--sizing-14);
max-inline-size: var(--sizing-70);
border-radius: var(--border-radius-elevation-3);
@each $color in colors {

View File

@ -0,0 +1 @@
export * from "./src";

View File

@ -0,0 +1,95 @@
import React, { forwardRef } from "react";
import { FocusScope } from "@react-aria/focus";
import { useDOMRef } from "@react-spectrum/utils";
import type { DOMRef } from "@react-types/shared";
import { useListState } from "@react-stately/list";
import styles from "./styles.module.css";
import type { ButtonGroupItemProps, ButtonGroupProps } from "./types";
import { ButtonGroupItem } from "./ButtonGroupItem";
import { useButtonGroup } from "./useButtonGroup";
const _ButtonGroup = <T extends object>(
props: ButtonGroupProps<T>,
ref: DOMRef<HTMLDivElement>,
) => {
const {
color = "accent",
density = "regular",
isDisabled,
onAction,
overflowMode = "collapse",
size = "medium",
variant = "filled",
...others
} = props;
const domRef = useDOMRef(ref);
const state = useListState({ ...props, suppressTextValueWarning: true });
const { buttonGroupProps, isMeasuring, orientation } = useButtonGroup(
props,
state,
domRef,
);
const children = [...state.collection];
const style = {
flexBasis: isMeasuring ? "100%" : undefined,
display: "flex",
};
return (
<FocusScope>
<div
style={{
...style,
}}
>
<div
className={styles.buttonGroup}
data-density={Boolean(density) ? density : undefined}
data-orientation={orientation}
data-overflow={overflowMode}
ref={domRef}
{...buttonGroupProps}
{...others}
>
{children.map((item) => {
if (Boolean(item.props.isSeparator)) {
return <div data-separator="" key={item.key} />;
}
return (
<ButtonGroupItem
color={
(item.props.color as ButtonGroupItemProps<object>["color"]) ??
color
}
icon={item.props.icon}
iconPosition={item.props.iconPosition}
isDisabled={
Boolean(state.disabledKeys.has(item.key)) ||
Boolean(isDisabled) ||
item.props.isDisabled
}
isLoading={item.props.isLoading}
item={item}
key={item.key}
onPress={() => onAction?.(item.key)}
size={Boolean(size) ? size : undefined}
state={state}
variant={
(item.props
.variant as ButtonGroupItemProps<object>["variant"]) ??
variant
}
/>
);
})}
</div>
</div>
</FocusScope>
);
};
export const ButtonGroup = forwardRef(_ButtonGroup);

View File

@ -0,0 +1,20 @@
import React, { forwardRef } from "react";
import { Button } from "@design-system/widgets";
import type { ButtonGroupItemProps } from "./types";
import type { ButtonRef as HeadlessButtonRef } from "@design-system/headless";
const _ButtonGroupItem = <T extends object>(
props: ButtonGroupItemProps<T>,
ref: HeadlessButtonRef,
) => {
const { color, item, variant, ...rest } = props;
return (
<Button color={color} ref={ref} variant={variant} {...rest}>
{item.rendered}
</Button>
);
};
export const ButtonGroupItem = forwardRef(_ButtonGroupItem);

View File

@ -0,0 +1,3 @@
export * from "./types";
export { ButtonGroup } from "./ButtonGroup";
export { ButtonGroupItem } from "./ButtonGroupItem";

View File

@ -0,0 +1,25 @@
@import "../../../shared/colors/colors.module.css";
.buttonGroup {
display: flex;
width: 100%;
gap: var(--inner-spacing-2);
& :is([data-button]) {
min-inline-size: fit-content;
}
&[data-orientation="vertical"] {
flex-direction: column;
align-items: center;
}
&[data-orientation="vertical"] :is([data-button]) {
min-inline-size: 100%;
max-inline-size: none;
}
& :is([data-separator]) {
flex-grow: 1;
}
}

View File

@ -14,7 +14,7 @@ export const ACTION_GROUP_ORIENTATIONS = {
export interface InheritedActionButtonProps
extends Pick<ButtonProps, "variant" | "color"> {}
export interface ActionGroupProps<T>
export interface ButtonGroupProps<T>
extends Omit<
SpectrumActionGroupProps<T>,
| "staticColor"
@ -36,7 +36,7 @@ export interface ActionGroupProps<T>
size?: Omit<keyof typeof SIZES, "large">;
}
export interface ActionGroupItemProps<T> extends ButtonProps {
export interface ButtonGroupItemProps<T> extends ButtonProps {
state: ListState<T>;
item: Node<T>;
}

View File

@ -0,0 +1,132 @@
import { createFocusManager } from "@react-aria/focus";
import type { ListState } from "@react-stately/list";
import { useCallback, type RefObject, useMemo } from "react";
import type { DOMAttributes, FocusableElement } from "@react-types/shared";
import type { ButtonGroupProps } from "../../../";
import {
useLayoutEffect,
useResizeObserver,
useValueEffect,
} from "@react-aria/utils";
export interface ButtonGroupAria {
buttonGroupProps: DOMAttributes;
isMeasuring: boolean;
orientation: ButtonGroupProps<object>["orientation"];
}
export function useButtonGroup<T>(
props: ButtonGroupProps<T>,
state: ListState<T>,
ref: RefObject<FocusableElement>,
): ButtonGroupAria {
const focusManager = createFocusManager(ref);
const onKeyDown = (e: React.KeyboardEvent<FocusableElement>) => {
if (!Boolean(e.currentTarget.contains(e.target as Node))) {
return;
}
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
e.preventDefault();
e.stopPropagation();
focusManager.focusNext({ wrap: true });
break;
case "ArrowLeft":
case "ArrowUp":
e.preventDefault();
e.stopPropagation();
focusManager.focusPrevious({ wrap: true });
break;
}
};
const [{ isMeasuring, orientation }, setOrientation] = useValueEffect({
orientation: props.orientation,
isMeasuring: false,
});
const updateOverflow = useCallback(() => {
if (props.orientation === "vertical") return;
const computeOrientation = () => {
if (ref.current) {
const listItems = Array.from(ref.current.children) as HTMLLIElement[];
const containerSize = ref.current.getBoundingClientRect().width;
const gap = toNumber(window.getComputedStyle(ref.current).gap);
let calculatedSize = 0;
for (const [i, item] of listItems.entries()) {
const itemWidth = item.getBoundingClientRect().width;
calculatedSize += itemWidth;
if (i !== 0) {
calculatedSize += gap;
}
if (calculatedSize >= containerSize) {
return "vertical";
}
}
}
return "horizontal";
};
setOrientation(function* () {
yield {
orientation: "horizontal",
isMeasuring: true,
};
const orientation = computeOrientation();
yield {
orientation: orientation,
isMeasuring: true,
};
if (isMeasuring) {
yield {
orientation: orientation,
isMeasuring: false,
};
}
});
}, [ref, state.collection, setOrientation, props.orientation]);
const parentRef = useMemo(
() => ({
get current() {
return ref.current?.parentElement;
},
}),
[ref],
);
useResizeObserver({
ref: parentRef,
onResize: updateOverflow,
});
useLayoutEffect(updateOverflow, [updateOverflow, state.collection]);
return {
buttonGroupProps: {
"aria-orientation": orientation,
onKeyDown,
},
isMeasuring,
orientation,
};
}
function toNumber(value: string) {
const parsed = parseInt(value, 10);
return isNaN(parsed) ? 0 : parsed;
}

View File

@ -0,0 +1,111 @@
import {
Flex,
ButtonGroup,
COLORS,
Item,
BUTTON_VARIANTS,
} from "@design-system/widgets";
import { Canvas, Meta, Story, ArgsTable, Source } from "@storybook/addon-docs";
<Meta title="Design-system/Widgets/Button Group" component={ButtonGroup} />
# Button Group
A `ButtonGroup` is a group of buttons that are visually connected together.
The `Item` accepts the same props as the `Button` except `variant` and `color`.
More information about `Button` props you can find [here](?path=/docs/design-system-widgets-button--docs).
export const Template = (args) => {
return (
<ButtonGroup {...args}>
<Item>Option 1</Item>
<Item>Option 2</Item>
<Item>Option 3</Item>
</ButtonGroup>
);
};
<Canvas>
<Story name="Button Group">{Template.bind({})}</Story>
</Canvas>
<ArgsTable story="Button Group" of={ButtonGroup} />
## Semantic
`Button` component has 3 visual style variants and 5 semantic color options
<Story name="Semantic">
<Flex direction="column" gap="spacing-4" alignItems="center" width="100%">
{Object.values(BUTTON_VARIANTS).map((variant) =>
Object.values(COLORS).map((color) => (
<ButtonGroup
variant={variant}
color={color}
key={`${variant}-${color}`}
>
<Item>
{variant} {color}
</Item>
<Item>
{variant} {color}
</Item>
<Item>
{variant} {color}
</Item>
</ButtonGroup>
)),
)}
</Flex>
</Story>
## Orientation
<Story name="Orientation horizontal">{Template.bind({})}</Story>
<Story
name="Orientation vertical"
args={{
orientation: "vertical",
}}
>
{Template.bind({})}
</Story>
# Responsive
<Story name="Responsive">
<div style={{ width: 150 }}>
<ButtonGroup variant="outlined">
<Item key="option-1">Option 1</Item>
<Item key="option-2">Option 2</Item>
<Item key="option-3" icon="file">
{"Option 3"}
</Item>
<Item key="option-4" icon="trash" iconPosition="end" color="negative">
{"Option 4"}
</Item>
</ButtonGroup>
</div>
</Story>
## ButtonGroupItem usage
<Story name="ButtonGroupItem usage">
<ButtonGroup>
<Item isDisabled>Option 2</Item>
<Item isLoading>Option 2</Item>
<Item>Option 3</Item>
</ButtonGroup>
</Story>
<Source
dark
code={`
<ButtonGroup>
<Item>Option 2</Item>
<Item isLoading>Option 2</Item>
<Item>Option 3</Item>
</ButtonGroup>
`}
/>

View File

@ -1,14 +1,14 @@
import React from "react";
import styles from "./styles.module.css";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function FallbackIcon(props: any) {
// adding height and width to svg to avoid expanded svg when lazy loading
return (
<svg
// adding styles of icon here as well so that icon takes appropriate space even when the actual icon is not loaded
className={styles.icon}
data-icon=""
data-testid="t--fallback-icon"
height={0}
width={0}
{...props}
/>
);

View File

@ -1,5 +1,5 @@
import type { Ref } from "react";
import React, { Suspense, forwardRef, lazy } from "react";
import React, { Suspense, forwardRef, lazy, useMemo } from "react";
import { useThemeContext } from "@design-system/theming";
import { Icon as HeadlessIcon } from "@design-system/headless";
@ -13,40 +13,51 @@ const _Icon = (props: IconProps, ref: Ref<SVGSVGElement>) => {
const theme = useThemeContext();
const filled = theme.iconStyle === "filled" || filledProp;
let Icon: React.ComponentType | null = null;
const Icon = useMemo(() => {
let Icon: React.ComponentType | null = null;
if (icon !== undefined) {
Icon = icon as React.ComponentType;
} else if (name !== undefined) {
const pascalName = ICONS[name];
if (icon !== undefined) {
Icon = icon as React.ComponentType;
} else if (name !== undefined) {
const pascalName = ICONS[name];
Icon = lazy(async () =>
import("@tabler/icons-react").then((module) => {
if (Boolean(filled)) {
const filledVariant = `${pascalName}Filled`;
Icon = lazy(async () =>
import("@tabler/icons-react").then((module) => {
if (Boolean(filled)) {
const filledVariant = `${pascalName}Filled`;
if (filledVariant in module) {
if (filledVariant in module) {
return {
default: module[filledVariant] as React.ComponentType,
};
}
}
if (pascalName in module) {
return {
default: module[filledVariant] as React.ComponentType,
default: module[pascalName] as React.ComponentType,
};
}
}
if (pascalName in module) {
return {
default: module[pascalName] as React.ComponentType,
};
}
return { default: FallbackIcon };
}),
);
} else {
Icon = FallbackIcon;
}
return { default: FallbackIcon };
}),
);
} else {
Icon = FallbackIcon;
}
return Icon;
}, [name, icon, filled]);
return (
<Suspense fallback={<FallbackIcon {...rest} />}>
<Suspense
fallback={
<FallbackIcon
className={styles.icon}
data-size={Boolean(size) ? size : undefined}
{...rest}
/>
}
>
<HeadlessIcon
className={styles.icon}
data-size={Boolean(size) ? size : undefined}

View File

@ -4,9 +4,11 @@ import type { ItemProps as HeadlessItemProps } from "@react-types/shared";
import type { IconProps } from "../../Icon";
import type { COLORS } from "../../../shared";
import type { ButtonProps } from "../../Button";
interface ItemProps<T> extends HeadlessItemProps<T> {
color?: keyof typeof COLORS;
variant?: ButtonProps["variant"];
icon?: IconProps["name"];
iconPosition?: "start" | "end";
isLoading?: boolean;
@ -24,6 +26,8 @@ _Item.getCollectionNode = <T,>(props: ItemProps<T>) => {
// @ts-expect-error this method is hidden by the types. See the source code of Item from Spectrum for more context.
return HeadlessItem.getCollectionNode({
...rest,
color,
// TODO(pawan): Check why we need [data-color] here.
["data-color"]: Boolean(color) ? color : undefined,
});
};

View File

@ -5,6 +5,7 @@ import { Text } from "../../Text";
import { IconButton } from "../../IconButton";
import { Flex } from "../../Flex";
import { useId } from "@floating-ui/react";
import styles from "./styles.module.css";
import type { ModalHeaderProps } from "./types";
@ -21,8 +22,19 @@ export const ModalHeader = (props: ModalHeaderProps) => {
}, [id, setLabelId]);
return (
<Flex alignItems="center" gap="spacing-4" justifyContent="space-between">
<Text id={id} lineClamp={1} title={title} variant="caption">
<Flex
alignItems="center"
className={styles.header}
gap="spacing-4"
justifyContent="space-between"
>
<Text
fontWeight={600}
id={id}
lineClamp={1}
title={title}
variant="subtitle"
>
{title}
</Text>
<IconButton icon="x" onPress={() => setOpen(false)} variant="ghost" />

View File

@ -14,22 +14,27 @@
outline: none;
display: flex;
flex-direction: column;
gap: var(--outer-spacing-4);
padding-inline: var(--outer-spacing-4);
padding-block: var(--outer-spacing-4);
gap: var(--inner-spacing-3);
padding-inline: var(--inner-spacing-4);
padding-block: var(--inner-spacing-3);
flex: 1;
}
.overlay .content .header {
padding-inline: 0 calc(var(--inner-spacing-2));
margin-inline: 0 calc(-1 * var(--inner-spacing-4));
}
.overlay .content .body {
overflow: auto;
padding-inline: var(--outer-spacing-4);
margin-inline: calc(var(--outer-spacing-4) * -1);
padding-inline: var(--inner-spacing-4);
margin-inline: calc(var(--inner-spacing-4) * -1);
}
.overlay [role="dialog"] {
display: flex;
max-inline-size: calc(100% - var(--outer-spacing-8));
max-block-size: calc(100% - var(--outer-spacing-8));
max-inline-size: calc(100% - var(--inner-spacing-8));
max-block-size: calc(100% - var(--inner-spacing-8));
}
.overlay [role="dialog"][data-size="small"] {

View File

@ -18,6 +18,7 @@ export * from "./components/Menu";
export * from "./components/Modal";
export * from "./components/TagGroup";
export * from "./components/ActionGroup";
export * from "./components/ButtonGroup";
export * from "./utils";
export * from "./styles";

View File

@ -148,6 +148,7 @@ export interface UpdateActionNameRequest {
newName: string;
oldName: string;
moduleId?: string;
workflowId?: string;
contextType?: ActionParentEntityTypeInterface;
}

View File

@ -0,0 +1,15 @@
/**
* Code splitting helper for audit logs
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const logActionExecutionForAudit = (payload: {
actionId: string;
pageId: string;
collectionId: string;
actionName: string;
pageName: string;
}) => {
return {
type: "",
};
};

View File

@ -23,6 +23,7 @@ export interface UpdateJSObjectNameRequest {
newName: string;
oldName: string;
moduleId?: string;
workflowId?: string;
contextType?: ActionParentEntityTypeInterface;
}

View File

@ -44,7 +44,7 @@ export const generateDataTreeJSAction = (
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
meta[action.name] = {
arguments: action.actionConfiguration.jsArguments,
arguments: action.actionConfiguration?.jsArguments,
confirmBeforeExecute: !!action.confirmBeforeExecute,
};
bindingPaths[action.name] = EvaluationSubstitutionType.SMART_SUBSTITUTE;

View File

@ -55,3 +55,5 @@ export const doesPluginRequireDatasource = (plugin: Plugin | undefined) => {
? plugin.requiresDatasource
: true;
};
export enum APPSMITH_NAMESPACED_FUNCTIONS {}

View File

@ -42,8 +42,6 @@ export const FEATURE_FLAG = {
release_side_by_side_ide_enabled: "release_side_by_side_ide_enabled",
release_global_add_pane_enabled: "release_global_add_pane_enabled",
ab_appsmith_ai_query: "ab_appsmith_ai_query",
ab_flip_primary_secondary_ctas_dsform_enabled:
"ab_flip_primary_secondary_ctas_dsform_enabled",
rollout_consolidated_page_load_fetch_enabled:
"rollout_consolidated_page_load_fetch_enabled",
ab_start_with_data_default_enabled: "ab_start_with_data_default_enabled",
@ -90,7 +88,6 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
release_side_by_side_ide_enabled: false,
release_global_add_pane_enabled: false,
ab_appsmith_ai_query: false,
ab_flip_primary_secondary_ctas_dsform_enabled: false,
rollout_consolidated_page_load_fetch_enabled: false,
ab_start_with_data_default_enabled: false,
release_actions_redesign_enabled: false,

View File

@ -97,7 +97,10 @@ export const handlers = {
isMainJSCollection: true,
displayName: "Main",
}
: action.payload.data,
: {
...action.payload.data,
isMainJSCollection: jsCollection.config.isMainJSCollection,
},
activeJSActionId:
findIndex(jsCollection.config.actions, {
id: jsCollection.activeJSActionId,
@ -123,7 +126,10 @@ export const handlers = {
isMainJSCollection: true,
displayName: "Main",
}
: action.payload.data,
: {
...action.payload.data,
isMainJSCollection: a.config.isMainJSCollection,
},
};
return a;
}),

View File

@ -66,6 +66,14 @@ import {
} from "pages/Editor/Explorer/ExplorerIcons";
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
export enum GROUP_TYPES {
API = "APIs",
JS_ACTIONS = "JS Objects",
AI = "AI Queries",
WORKFLOWS = "Workflows",
PACKAGES = "Packages",
}
export const getEntities = (state: AppState): AppState["entities"] =>
state.entities;
@ -1042,20 +1050,20 @@ export const selectFilesForExplorer = createSelector(
...workflowActions,
...workflowJsActions,
].reduce((acc, file) => {
let group = "";
let group;
if (file.config.pluginType === PluginType.JS) {
group = "JS Objects";
group = GROUP_TYPES.JS_ACTIONS;
} else if (file.config.pluginType === PluginType.API) {
group = isEmbeddedRestDatasource(file.config.datasource)
? "APIs"
: datasourceIdToNameMap[file.config.datasource.id] ?? "APIs";
? GROUP_TYPES.API
: datasourceIdToNameMap[file.config.datasource.id] ?? GROUP_TYPES.API;
} else if (file.config.pluginType === PluginType.AI) {
group = isEmbeddedAIDataSource(file.config.datasource)
? "AI Queries"
: datasourceIdToNameMap[file.config.datasource.id] ?? "AI Queries";
? GROUP_TYPES.AI
: datasourceIdToNameMap[file.config.datasource.id] ?? GROUP_TYPES.AI;
} else if (file.config.pluginType === PluginType.INTERNAL) {
// TODO: Add a group for internal actions, currently only Workflow actions are internal
group = "Workflows";
group = GROUP_TYPES.WORKFLOWS;
} else {
group = datasourceIdToNameMap[file.config.datasource.id];
}

View File

@ -0,0 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
export const logMainJsActionExecution = (
actionId: string,
isSuccess: boolean,
collectionId: string,
isDirty: boolean,
) => {};

View File

@ -15,6 +15,7 @@ import type {
} from "@appsmith/entities/DataTree/types";
import type { FieldEntityInformation } from "components/editorComponents/CodeEditor/EditorConfig";
import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
import { eeAppsmithAutocompleteDefs } from "@appsmith/utils/autocomplete/helpers";
export const entityDefinitions = {
APPSMITH: (entity: AppsmithEntity, extraDefsToDefine: ExtraDef) => {
@ -88,6 +89,7 @@ export const entityDefinitions = {
"https://docs.appsmith.com/reference/appsmith-framework/context-object#geolocationclearwatch",
},
},
...eeAppsmithAutocompleteDefs(generatedTypeDef),
};
}
return generatedTypeDef;

View File

@ -0,0 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { Def } from "tern";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const eeAppsmithAutocompleteDefs = (generatedTypeDef: Def) => ({});

View File

@ -11,7 +11,7 @@ import type { EvalContext } from "workers/Evaluation/evaluate";
import type { EvaluationVersion } from "@appsmith/api/ApplicationApi";
import { addFn } from "workers/Evaluation/fns/utils/fnGuard";
import {
entityFns,
getEntityFunctions,
getPlatformFunctions,
} from "@appsmith/workers/Evaluation/fns";
import { getEntityForEvalContext } from "workers/Evaluation/getEntityForContext";
@ -67,7 +67,7 @@ export const addDataTreeToContext = (args: {
if (skipEntityFunctions) continue;
for (const entityFn of entityFns) {
for (const entityFn of getEntityFunctions()) {
if (!entityFn.qualifier(entity)) continue;
const func = entityFn.fn(entity, entityName);
const fullPath = `${entityFn.path || `${entityName}.${entityFn.name}`}`;
@ -166,7 +166,7 @@ export const getAllAsyncFunctions = (
let allAsyncFunctions: Record<string, true> = {};
const dataTreeEntries = Object.entries(dataTree);
for (const [entityName, entity] of dataTreeEntries) {
for (const entityFn of entityFns) {
for (const entityFn of getEntityFunctions()) {
if (!entityFn.qualifier(entity)) continue;
const fullPath = `${entityFn.path || `${entityName}.${entityFn.name}`}`;
allAsyncFunctions[fullPath] = true;

View File

@ -1,3 +1,5 @@
import { APPSMITH_NAMESPACED_FUNCTIONS as EE_APPSMITH_NAMESPACED_FUNCTIONS } from "@appsmith/entities/Engine/actionHelpers";
export enum APPSMITH_GLOBAL_FUNCTIONS {
navigateTo = "navigateTo",
showAlert = "showAlert",
@ -30,6 +32,7 @@ export const AppsmithFunction = {
...APPSMITH_INTEGRATIONS,
...APPSMITH_GLOBAL_FUNCTIONS,
...APPSMITH_NAMESPACED_FUNCTIONS,
...EE_APPSMITH_NAMESPACED_FUNCTIONS,
};
export const AppsmithFunctionsWithFields = [

View File

@ -461,6 +461,7 @@ function getApiAndQueryOptions(
action.config.pluginType === PluginType.API ||
action.config.pluginType === PluginType.SAAS ||
action.config.pluginType === PluginType.REMOTE ||
action.config.pluginType === PluginType.INTERNAL ||
action.config.pluginType === PluginType.AI,
);

View File

@ -88,6 +88,9 @@ function GetIconForAction(
case AppsmithFunction.postWindowMessage:
return () => <Icon name="chat-upload-icon" />;
default:
return () => <Icon name="js" />;
}
}

View File

@ -35,6 +35,7 @@ export const useActions = () => {
(action) =>
action.config.pluginType === PluginType.SAAS ||
action.config.pluginType === PluginType.REMOTE ||
action.config.pluginType === PluginType.INTERNAL ||
action.config.pluginType === PluginType.AI,
)
.map((action) => action.config);

View File

@ -15,8 +15,17 @@ interface State {
focusedIndex: number | null;
}
interface MenuItem {
id: string;
label: string;
isDisabled: boolean;
isVisible: boolean;
widgetId: string;
itemType: "SEPARATOR" | "BUTTON";
}
class ButtonListControl extends BaseControl<
ControlProps & { allowSeparators?: boolean },
ControlProps & { allowSeparators?: boolean; allowSpatialGrouping?: boolean },
State
> {
constructor(props: ControlProps) {
@ -38,14 +47,7 @@ class ButtonListControl extends BaseControl<
}
getMenuItems = () => {
const menuItems: Array<{
id: string;
label: string;
isDisabled: boolean;
isVisible: boolean;
widgetId: string;
isSeparator: boolean;
}> =
const menuItems: MenuItem[] =
isString(this.props.propertyValue) ||
isUndefined(this.props.propertyValue)
? []
@ -68,6 +70,7 @@ class ButtonListControl extends BaseControl<
onEdit = (index: number) => {
const menuItems = this.getMenuItems();
const targetMenuItem = menuItems[index];
this.props.openNextPanel({
index,
...targetMenuItem,
@ -76,6 +79,10 @@ class ButtonListControl extends BaseControl<
};
render() {
const hasSeparator = this.getMenuItems().some(
(item: MenuItem) => item.itemType === "SEPARATOR",
);
return (
<div className="flex flex-col gap-1">
<DraggableListControl
@ -99,8 +106,9 @@ class ButtonListControl extends BaseControl<
updateOption={this.updateOption}
/>
<Flex justifyContent="end">
{this.props.allowSeparators && (
<Flex gap="spaces-3" justifyContent="end">
{(this.props.allowSeparators ||
(this.props.allowSpatialGrouping && !hasSeparator)) && (
<Button
className="self-end"
kind="tertiary"
@ -185,13 +193,14 @@ class ButtonListControl extends BaseControl<
isDisabled: false,
itemType: isSeparator ? "SEPARATOR" : "BUTTON",
isVisible: true,
buttonVariant: "filled",
},
};
if (this.props.widgetProperties.type === "BUTTON_GROUP_WIDGET") {
/**
/**c
* These properties are required for "BUTTON_GROUP_WIDGET" but not for
* "WDS_BUTTON_GROUP_WIDGET"
* "WDS_TOOLBAR_BUTTONS_GROUP_WIDGET"
*/
const optionalButtonGroupItemProperties = {
menuItems: {},
@ -207,6 +216,21 @@ class ButtonListControl extends BaseControl<
};
}
// if the widget is a WDS_INLINE_BUTTONS_WIDGET, and button already have filled button variant in groupButtons,
// then we should add a secondary button ( outlined button ) instead of simple button
if (this.props.widgetProperties.type === "WDS_INLINE_BUTTONS_WIDGET") {
const filledButtonVariant = groupButtonsArray.find(
(groupButton: any) => groupButton.buttonVariant === "filled",
);
if (filledButtonVariant) {
groupButtons[newGroupButtonId] = {
...groupButtons[newGroupButtonId],
buttonVariant: "outlined",
};
}
}
this.updateProperty(this.props.propertyName, groupButtons);
};

View File

@ -204,7 +204,7 @@ export function DraggableListCard(props: RenderComponentProps) {
startIcon="settings-2-line"
/>
)}
{showDelete && (
{showDelete && !isSeparator && (
<Button
className="t--delete-column-btn"
isIconButton
@ -216,7 +216,10 @@ export function DraggableListCard(props: RenderComponentProps) {
startIcon="delete-bin-line"
/>
)}
{!showDelete && toggleVisibility && renderVisibilityIcon()}
{!showDelete &&
!isSeparator &&
toggleVisibility &&
renderVisibilityIcon()}
{/*
* Used in Table_Widget_V2's primary columns to enable/disable cell editability.
* Using a common name `showCheckbox` instead of showEditable or isEditable,

View File

@ -0,0 +1 @@
export * from "ce/actions/auditLogsAction";

View File

@ -0,0 +1 @@
export * from "ce/utils/analyticsHelpers";

View File

@ -0,0 +1 @@
export * from "ce/utils/autocomplete/helpers";

View File

@ -167,10 +167,6 @@ function DatasourceAuth({
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const isEnabledForTestPrimary = !!useFeatureFlag(
FEATURE_FLAG.ab_flip_primary_secondary_ctas_dsform_enabled,
);
const canManageDatasource = getHasManageDatasourcePermission(
isFeatureEnabled,
datasourcePermissions,
@ -336,11 +332,7 @@ function DatasourceAuth({
floatLeft={!isInsideReconnectModal}
isLoading={isTesting}
key={buttonType}
kind={
isEnabledForTestPrimary && pluginType === "DB"
? "primary"
: "secondary"
}
kind="secondary"
onClick={handleDatasourceTest}
size="md"
>
@ -387,11 +379,6 @@ function DatasourceAuth({
}
isLoading={isSaving}
key={buttonType}
kind={
isEnabledForTestPrimary && pluginType === "DB"
? "secondary"
: "primary"
}
onClick={
authType === AuthType.OAUTH2
? handleOauthDatasourceSave

View File

@ -23,7 +23,7 @@ import {
LintEntityTree,
type EntityTree,
} from "plugins/Linting/lib/entity/EntityTree";
import { entityFns } from "workers/Evaluation/fns";
import { getEntityFunctions } from "@appsmith/workers/Evaluation/fns";
class LintService {
cachedEntityTree: EntityTree | null;
@ -339,7 +339,7 @@ function filterDataPaths(paths: string[], entityTree: EntityTree) {
function getAllEntityActions(entityTree: EntityTree) {
const allEntityActions = new Set<string>();
for (const [entityName, entity] of Object.entries(entityTree.getRawTree())) {
for (const entityFnDescription of entityFns) {
for (const entityFnDescription of getEntityFunctions()) {
if (entityFnDescription.qualifier(entity)) {
const fullPath = `${
entityFnDescription.path ||

View File

@ -1,6 +1,6 @@
import type { DataTreeEntity } from "entities/DataTree/dataTreeTypes";
import { isDataTreeEntity } from "@appsmith/workers/Evaluation/evaluationUtils";
import { entityFns } from "@appsmith/workers/Evaluation/fns";
import { getEntityFunctions } from "@appsmith/workers/Evaluation/fns";
import setters from "workers/Evaluation/setters";
export default function isEntityFunction(
@ -12,6 +12,8 @@ export default function isEntityFunction(
if (setters.has(entityName, propertyName)) return true;
const entityFns = getEntityFunctions();
return entityFns.find((entityFn) => {
const entityFnpropertyName = entityFn.path
? entityFn.path.split(".")[1]

View File

@ -32,6 +32,7 @@ export interface AppDataState {
canBeRequested: boolean;
currentPosition?: Partial<GeolocationPosition>;
};
workflows: Record<string, any>;
}
const initialState: AppDataState = {
@ -55,6 +56,7 @@ const initialState: AppDataState = {
canBeRequested: "geolocation" in navigator,
currentPosition: {},
},
workflows: {},
};
const appReducer = createReducer(initialState, {

View File

@ -16,6 +16,7 @@ import {
import {
getCurrentApplicationId,
getCurrentPageId,
getCurrentPageName,
getIsSavingEntity,
} from "selectors/editorSelectors";
import {
@ -99,6 +100,8 @@ import {
getJSActionPathNameToDisplay,
} from "@appsmith/utils/actionExecutionUtils";
import { getJsPaneDebuggerState } from "selectors/jsPaneSelectors";
import { logMainJsActionExecution } from "@appsmith/utils/analyticsHelpers";
import { logActionExecutionForAudit } from "@appsmith/actions/auditLogsAction";
export interface GenerateDefaultJSObjectProps {
name: string;
@ -455,7 +458,21 @@ export function* handleExecuteJSFunctionSaga(data: {
},
});
if (!!collection.isMainJSCollection)
logMainJsActionExecution(actionId, true, collectionId, isDirty);
yield put(
logActionExecutionForAudit({
actionName: action.name,
actionId: action.id,
collectionId: collectionId,
pageId: action.pageId,
pageName: yield select(getCurrentPageName),
}),
);
const jsActionNameToDisplay = getJSActionNameToDisplay(action);
AppsmithConsole.info({
text: createMessage(JS_EXECUTION_SUCCESS),
source: {
@ -489,6 +506,10 @@ export function* handleExecuteJSFunctionSaga(data: {
}),
);
}
if (!!collection.isMainJSCollection)
logMainJsActionExecution(actionId, false, collectionId, false);
AppsmithConsole.addErrors([
{
payload: {

View File

@ -18,6 +18,30 @@ declare global {
}
}
const parentContextTypeTokens = ["pkg", "workflow"];
/**
* Function to check the current URL and return the parent context.
* For app, function was returning app name due to the way app urls are structured
* So this function will only return the parent context for pkg and workflow
* @param location current location object based on URL
* @returns object {id, type} where type is either pkg or workflow and id is the id of the pkg or workflow
*/
function getParentContextFromURL(location: Location) {
const pathSplit = location.pathname.split("/");
let type = parentContextTypeTokens[0];
const editorIndex = pathSplit.findIndex((path) =>
parentContextTypeTokens.includes(path),
);
if (editorIndex !== -1) {
type = pathSplit[editorIndex];
const id = pathSplit[editorIndex + 1];
return { id, type };
}
}
function getApplicationId(location: Location) {
const pathSplit = location.pathname.split("/");
const applicationsIndex = pathSplit.findIndex(
@ -141,6 +165,7 @@ class AnalyticsUtil {
const windowDoc: any = window;
let finalEventData = eventData;
const userData = AnalyticsUtil.user;
const parentContext = getParentContextFromURL(windowDoc.location);
const instanceId = AnalyticsUtil.instanceId;
const appId = getApplicationId(windowDoc.location);
const { appVersion, segment } = getAppsmithConfigs();
@ -150,7 +175,7 @@ class AnalyticsUtil {
user = {
userId: userData.username,
email: userData.email,
appId: appId,
appId,
source: "cloud",
};
} else {
@ -169,7 +194,12 @@ class AnalyticsUtil {
userData: user.userId === ANONYMOUS_USERNAME ? undefined : user,
};
}
finalEventData = { ...finalEventData, instanceId, version: appVersion.id };
finalEventData = {
...finalEventData,
instanceId,
version: appVersion.id,
...(parentContext ? { parentContext } : {}),
};
if (windowDoc.analytics) {
log.debug("Event fired", eventName, finalEventData);

View File

@ -65,7 +65,7 @@ import type BaseWidget from "./BaseWidget";
import ExternalWidget from "./ExternalWidget";
import { WDSTableWidget } from "./wds/WDSTableWidget";
import { WDSCurrencyInputWidget } from "./wds/WDSCurrencyInputWidget";
import { WDSButtonGroupWidget } from "./wds/WDSButtonGroupWidget";
import { WDSToolbarButtonsWidget } from "./wds/WDSToolbarButtonsWidget";
import { WDSPhoneInputWidget } from "./wds/WDSPhoneInputWidget";
import { WDSCheckboxGroupWidget } from "./wds/WDSCheckboxGroupWidget";
import { WDSSwitchWidget } from "./wds/WDSSwitchWidget";
@ -80,6 +80,7 @@ import { WDSParagraphWidget } from "./wds/WDSParagraphWidget";
import { WDSModalWidget } from "./wds/WDSModalWidget";
import { WDSStatBoxWidget } from "./wds/WDSStatBoxWidget";
import { WDSKeyValueWidget } from "./wds/WDSKeyValueWidget";
import { WDSInlineButtonsWidget } from "./wds/WDSInlineButtonsWidget";
const LegacyWidgets = [
CanvasWidget,
@ -156,7 +157,7 @@ const WDSWidgets = [
WDSIconButtonWidget,
WDSTableWidget,
WDSCurrencyInputWidget,
WDSButtonGroupWidget,
WDSToolbarButtonsWidget,
WDSPhoneInputWidget,
WDSCheckboxGroupWidget,
WDSSwitchWidget,
@ -171,6 +172,7 @@ const WDSWidgets = [
WDSModalWidget,
WDSStatBoxWidget,
WDSKeyValueWidget,
WDSInlineButtonsWidget,
];
const Widgets = [

View File

@ -1,3 +0,0 @@
import { WDSButtonGroupWidget } from "./widget";
export { WDSButtonGroupWidget };

View File

@ -1,13 +0,0 @@
import type { ActionGroupProps } from "@design-system/widgets";
import type { WidgetProps } from "widgets/BaseWidget";
import type { ButtonGroupItemComponentProps } from "../component/types";
export type ButtonsList = Record<string, ButtonGroupItemComponentProps>;
export interface ButtonGroupWidgetProps extends WidgetProps {
buttonColor: ActionGroupProps<object>["color"];
buttonVariant: ActionGroupProps<object>["variant"];
orientation: ActionGroupProps<object>["orientation"];
isVisible: boolean;
buttonsList: ButtonsList;
}

View File

@ -0,0 +1,78 @@
import type { Key } from "react";
import React, { useState } from "react";
import { ButtonGroup, Item } from "@design-system/widgets";
import type {
InlineButtonsComponentProps,
InlineButtonsItemComponentProps,
} from "./types";
import { sortBy } from "lodash";
export const InlineButtonsComponent = (props: InlineButtonsComponentProps) => {
const [loadingButtonIds, setLoadingButtonIds] = useState<
Array<InlineButtonsItemComponentProps["id"]>
>([]);
const sortedButtons = sortBy(
Object.keys(props.buttonsList)
.map((key) => props.buttonsList[key])
.filter((button) => {
return button.isVisible === true;
}),
["index"],
);
const disabledKeys = Object.keys(props.buttonsList)
.map((key) => props.buttonsList[key])
.filter((button) => {
return button.isDisabled === true;
})
.map((button) => button.id);
const onActionComplete = (button: InlineButtonsItemComponentProps) => {
const newLoadingButtonIds = [...loadingButtonIds];
const index = newLoadingButtonIds.indexOf(button.id);
if (index > -1) {
newLoadingButtonIds.splice(index, 1);
}
setLoadingButtonIds(newLoadingButtonIds);
};
const onAction = (key: Key) => {
if (props.buttonsList[key].onClick) {
setLoadingButtonIds([...loadingButtonIds, key as string]);
props.onButtonClick(props.buttonsList[key].onClick, () =>
onActionComplete(props.buttonsList[key]),
);
}
};
return (
<ButtonGroup
color={props.color}
density={props.density}
disabledKeys={disabledKeys}
onAction={onAction}
orientation={props.orientation}
variant={props.variant}
>
{sortedButtons.map((button: InlineButtonsItemComponentProps) => {
return (
<Item
color={button.buttonColor}
icon={button.iconName}
iconPosition={button.iconAlign}
isLoading={loadingButtonIds.includes(button.id)}
isSeparator={button.itemType === "SEPARATOR"}
key={button.id}
variant={button.buttonVariant}
>
{button.label}
</Item>
);
})}
</ButtonGroup>
);
};

View File

@ -0,0 +1,35 @@
import type {
ButtonGroupProps,
ButtonProps,
IconProps,
} from "@design-system/widgets";
import type { ButtonsList } from "../widget/types";
import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionConstants";
export interface InlineButtonsComponentProps {
color?: ButtonGroupProps<object>["color"];
variant?: ButtonGroupProps<object>["variant"];
orientation: ButtonGroupProps<object>["orientation"];
buttonsList: ButtonsList;
onButtonClick: (
onClick?: string,
callback?: (result: ExecutionResult) => void,
) => void;
density?: ButtonGroupProps<object>["density"];
}
export interface InlineButtonsItemComponentProps {
label?: string;
isVisible?: boolean;
isLoading?: boolean;
isDisabled?: boolean;
widgetId: string;
id: string;
index: number;
iconName?: IconProps["name"];
iconAlign?: ButtonProps["iconPosition"];
onClick?: string;
itemType: "SEPARATOR" | "BUTTON";
buttonVariant?: ButtonProps["variant"];
buttonColor?: ButtonProps["color"];
}

View File

@ -0,0 +1,50 @@
import { ResponsiveBehavior } from "layoutSystems/common/utils/constants";
import type { WidgetDefaultProps } from "WidgetProvider/constants";
export const defaultsConfig = {
widgetName: "InlineButtons",
isDisabled: false,
isVisible: true,
version: 1,
animateLoading: true,
responsiveBehavior: ResponsiveBehavior.Fill,
buttonsList: {
button1: {
label: "Delete",
isVisible: true,
isDisabled: false,
id: "button1",
index: 0,
buttonVariant: "outlined",
buttonColor: "negative",
},
button2: {
label: "Separator",
isVisible: true,
isDisabled: false,
id: "button2",
index: 1,
itemType: "SEPARATOR",
},
button3: {
label: "Cancel",
isVisible: true,
isDisabled: false,
widgetId: "",
id: "button3",
index: 2,
buttonVariant: "outlined",
buttonColor: "accent",
},
button4: {
label: "Save",
isVisible: true,
isDisabled: false,
widgetId: "",
id: "button4",
index: 3,
buttonVariant: "filled",
buttonColor: "accent",
},
},
} as unknown as WidgetDefaultProps;

View File

@ -4,7 +4,7 @@ import ThumbnailSVG from "../thumbnail.svg";
import { WIDGET_TAGS } from "constants/WidgetConstants";
export const metaConfig = {
name: "Button Group",
name: "Inline Buttons",
iconSVG: IconSVG,
thumbnailSVG: ThumbnailSVG,
needsMeta: false,

View File

@ -0,0 +1,209 @@
import { BUTTON_VARIANTS, COLORS } from "@design-system/widgets";
import { ValidationTypes } from "constants/WidgetValidation";
import { capitalize } from "lodash";
export const propertyPaneContentConfig = [
{
sectionName: "Data",
children: [
{
helpText: "Group Buttons",
propertyName: "buttonsList",
controlType: "GROUP_BUTTONS",
allowSpatialGrouping: true,
label: "Buttons",
dependencies: ["childStylesheet", "orientation"],
isBindProperty: false,
isTriggerProperty: false,
panelConfig: {
editableTitle: true,
titlePropertyName: "label",
panelIdPropertyName: "id",
updateHook: (
props: any,
propertyPath: string,
propertyValue: string,
) => {
return [
{
propertyPath,
propertyValue,
},
];
},
contentChildren: [
{
sectionName: "Label",
children: [
{
propertyName: "label",
helpText: "Sets the label of the button",
label: "Text",
controlType: "INPUT_TEXT",
placeholderText: "Enter label",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
],
},
{
sectionName: "General",
children: [
{
propertyName: "isVisible",
helpText: "Controls the visibility of the widget",
label: "Visible",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "isDisabled",
helpText: "Disables input to the widget",
label: "Disabled",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
],
},
{
sectionName: "Events",
children: [
{
helpText: "when the button is clicked",
propertyName: "onClick",
label: "onClick",
controlType: "ACTION_SELECTOR",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: true,
},
],
},
],
styleChildren: [
{
sectionName: "General",
children: [
{
propertyName: "buttonVariant",
label: "Button variant",
controlType: "ICON_TABS",
fullWidth: true,
helpText: "Sets the variant of the button",
options: Object.values(BUTTON_VARIANTS).map((variant) => ({
label: capitalize(variant),
value: variant,
})),
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
allowedValues: Object.values(BUTTON_VARIANTS),
default: BUTTON_VARIANTS.filled,
},
},
},
{
propertyName: "buttonColor",
label: "Button color",
controlType: "DROP_DOWN",
fullWidth: true,
helpText: "Sets the semantic color of the button",
options: Object.values(COLORS).map((semantic) => ({
label: capitalize(semantic),
value: semantic,
})),
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
allowedValues: Object.values(COLORS),
default: COLORS.accent,
},
},
},
],
},
{
sectionName: "Icon",
children: [
{
propertyName: "iconName",
label: "Icon",
helpText: "Sets the icon to be used for a button",
controlType: "ICON_SELECT_V2",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "iconAlign",
label: "Position",
helpText: "Sets the icon alignment of the button",
controlType: "ICON_TABS",
fullWidth: false,
options: [
{
startIcon: "skip-left-line",
value: "start",
},
{
startIcon: "skip-right-line",
value: "end",
},
],
isBindProperty: false,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
allowedValues: ["start", "end"],
},
},
},
],
},
],
},
},
],
},
{
sectionName: "General",
children: [
{
helpText: "Controls the visibility of the widget",
propertyName: "isVisible",
label: "Visible",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "animateLoading",
label: "Animate loading",
controlType: "SWITCH",
helpText: "Controls the loading of the widget",
defaultValue: true,
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
],
},
];

View File

@ -0,0 +1 @@
export { propertyPaneContentConfig } from "./contentConfig";

View File

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

View File

@ -0,0 +1,3 @@
import { WDSInlineButtonsWidget } from "./widget";
export { WDSInlineButtonsWidget };

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,90 @@
import React from "react";
import type { SetterConfig } from "entities/AppTheming";
import type { WidgetState } from "widgets/BaseWidget";
import BaseWidget from "widgets/BaseWidget";
import {
metaConfig,
defaultsConfig,
autocompleteConfig,
propertyPaneContentConfig,
settersConfig,
anvilConfig,
} from "./../config";
import type { InlineButtonsWidgetProps } from "./types";
import { InlineButtonsComponent } from "../component";
import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionConstants";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import type { AnvilConfig } from "WidgetProvider/constants";
class WDSInlineButtonsWidget extends BaseWidget<
InlineButtonsWidgetProps,
WidgetState
> {
constructor(props: InlineButtonsWidgetProps) {
super(props);
}
static type = "WDS_INLINE_BUTTONS_WIDGET";
static getConfig() {
return metaConfig;
}
static getDefaults() {
return defaultsConfig;
}
static getAutocompleteDefinitions() {
return autocompleteConfig;
}
static getPropertyPaneContentConfig() {
return propertyPaneContentConfig;
}
static getPropertyPaneStyleConfig() {
return [];
}
static getSetterConfig(): SetterConfig {
return settersConfig;
}
static getAnvilConfig(): AnvilConfig | null {
return anvilConfig;
}
onButtonClick = (
onClick: string | undefined,
callback?: (result: ExecutionResult) => void,
) => {
if (onClick) {
super.executeAction({
triggerPropertyName: "onClick",
dynamicString: onClick,
event: {
type: EventType.ON_CLICK,
callback: callback,
},
});
}
return;
};
getWidgetView() {
return (
<InlineButtonsComponent
buttonsList={this.props.buttonsList}
color={this.props.buttonColor}
density={this.props.density}
key={this.props.widgetId}
onButtonClick={this.onButtonClick}
orientation={this.props.orientation}
variant={this.props.buttonVariant}
/>
);
}
}
export { WDSInlineButtonsWidget };

View File

@ -0,0 +1,13 @@
import type { ButtonGroupProps } from "@design-system/widgets";
import type { WidgetProps } from "widgets/BaseWidget";
import type { InlineButtonsItemComponentProps } from "../component/types";
export type ButtonsList = Record<string, InlineButtonsItemComponentProps>;
export interface InlineButtonsWidgetProps extends WidgetProps {
buttonColor: ButtonGroupProps<object>["color"];
buttonVariant: ButtonGroupProps<object>["variant"];
orientation: ButtonGroupProps<object>["orientation"];
isVisible: boolean;
buttonsList: ButtonsList;
}

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="76" fill="none"><rect width="44" height="39" x="14.5" y="19.5" fill="#fff" rx="2.5"/><rect width="44" height="39" x="14.5" y="19.5" stroke="#CDD5DF" rx="2.5"/><rect width="16" height="6" x="22" y="27" fill="#CDD5DF" rx="1"/><path stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round" d="M22 50.5h2.5m2.5 0h-2.5m-2.5-8 2.5-2v10M35 50.5h-5v-2.646a2 2 0 0 1 1.257-1.857l2.486-.994A2 2 0 0 0 35 43.146V42.5a2 2 0 0 0-2-2h-3M38 40.5h3a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2h-1.715M38 50.5h3a2 2 0 0 0 2-2v-1a2 2 0 0 0-2-2h-2"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="76" fill="none"><rect width="56" height="44" x="8" y="16" fill="#fff" rx="3"/><rect width="32" height="6" x="16" y="24" fill="#E3E8EF" rx="1"/><path stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round" d="M16 50.5h2.5m2.5 0h-2.5m-2.5-8 2.5-2v10M29 50.5h-5v-2.646a2 2 0 0 1 1.257-1.857l2.486-.994A2 2 0 0 0 29 43.146V42.5a2 2 0 0 0-2-2h-3M32 40.5h3a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2h-1.715M32 50.5h3a2 2 0 0 0 2-2v-1a2 2 0 0 0-2-2h-2"/><rect width="12" height="12" x="43.5" y="39.5" fill="#FFEEE5" rx="1.5"/><rect width="12" height="12" x="43.5" y="39.5" stroke="#CC3D00" rx="1.5"/><path stroke="#CC3D00" stroke-linecap="round" d="m47 48 5-5"/><path stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round" d="M49 43h3v3"/></svg>

Before

Width:  |  Height:  |  Size: 588 B

After

Width:  |  Height:  |  Size: 801 B

View File

@ -2,14 +2,16 @@ import type { Key } from "react";
import React, { useState } from "react";
import { ActionGroup, Item } from "@design-system/widgets";
import type {
ButtonGroupComponentProps,
ButtonGroupItemComponentProps,
ToolbarButtonsComponentProps,
ToolbarButtonsItemComponentProps,
} from "./types";
import { sortBy } from "lodash";
export const ButtonGroupComponent = (props: ButtonGroupComponentProps) => {
export const ToolbarButtonsComponent = (
props: ToolbarButtonsComponentProps,
) => {
const [loadingButtonIds, setLoadingButtonIds] = useState<
Array<ButtonGroupItemComponentProps["id"]>
Array<ToolbarButtonsItemComponentProps["id"]>
>([]);
const sortedButtons = sortBy(
@ -28,7 +30,7 @@ export const ButtonGroupComponent = (props: ButtonGroupComponentProps) => {
})
.map((button) => button.id);
const onActionComplete = (button: ButtonGroupItemComponentProps) => {
const onActionComplete = (button: ToolbarButtonsItemComponentProps) => {
const newLoadingButtonIds = [...loadingButtonIds];
const index = newLoadingButtonIds.indexOf(button.id);
@ -58,7 +60,7 @@ export const ButtonGroupComponent = (props: ButtonGroupComponentProps) => {
orientation={props.orientation}
variant={props.variant}
>
{sortedButtons.map((button: ButtonGroupItemComponentProps) => {
{sortedButtons.map((button: ToolbarButtonsItemComponentProps) => {
return (
<Item
icon={button.iconName}

View File

@ -1,24 +1,24 @@
import type {
ActionGroupProps,
ButtonGroupProps,
ButtonProps,
IconProps,
} from "@design-system/widgets";
import type { ButtonsList } from "../widget/types";
import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionConstants";
export interface ButtonGroupComponentProps {
color?: ActionGroupProps<object>["color"];
variant?: ActionGroupProps<object>["variant"];
orientation: ActionGroupProps<object>["orientation"];
export interface ToolbarButtonsComponentProps {
color?: ButtonGroupProps<object>["color"];
variant?: ButtonGroupProps<object>["variant"];
orientation: ButtonGroupProps<object>["orientation"];
buttonsList: ButtonsList;
onButtonClick: (
onClick?: string,
callback?: (result: ExecutionResult) => void,
) => void;
density?: ActionGroupProps<object>["density"];
density?: ButtonGroupProps<object>["density"];
}
export interface ButtonGroupItemComponentProps {
export interface ToolbarButtonsItemComponentProps {
label?: string;
isVisible?: boolean;
isLoading?: boolean;

View File

@ -0,0 +1,11 @@
import type { AnvilConfig } from "WidgetProvider/constants";
export const anvilConfig: AnvilConfig = {
isLargeWidget: false,
widgetSize: {
minWidth: {
base: "100%",
"180px": "sizing-14",
},
},
};

View File

@ -0,0 +1,8 @@
import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils";
export const autocompleteConfig = {
"!doc":
"The Button group widget represents a set of buttons in a group. Group can have simple buttons or menu buttons with drop-down items.",
"!url": "https://docs.appsmith.com/widget-reference/button-group",
isVisible: DefaultAutocompleteDefinitions.isVisible,
};

View File

@ -2,7 +2,7 @@ import { ResponsiveBehavior } from "layoutSystems/common/utils/constants";
import type { WidgetDefaultProps } from "WidgetProvider/constants";
export const defaultsConfig = {
widgetName: "ButtonGroup",
widgetName: "ToolbarButtons",
orientation: "horizontal",
buttonVariant: "filled",
buttonColor: "accent",

View File

@ -0,0 +1,6 @@
export * from "./propertyPaneConfig";
export { autocompleteConfig } from "./autocompleteConfig";
export { defaultsConfig } from "./defaultsConfig";
export { metaConfig } from "./metaConfig";
export { settersConfig } from "./settersConfig";
export { anvilConfig } from "./anvilConfig";

View File

@ -0,0 +1,14 @@
import IconSVG from "../icon.svg";
import ThumbnailSVG from "../thumbnail.svg";
import { WIDGET_TAGS } from "constants/WidgetConstants";
export const metaConfig = {
name: "Toolbar Buttons",
iconSVG: IconSVG,
thumbnailSVG: ThumbnailSVG,
needsMeta: false,
isCanvas: false,
searchTags: ["click", "submit"],
tags: [WIDGET_TAGS.BUTTONS],
};

View File

@ -69,27 +69,6 @@ export const propertyPaneStyleConfig = [
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "density",
helpText: "Controls the density of the widget",
label: "Density",
controlType: "ICON_TABS",
fullWidth: true,
options: [
{
label: "Regular",
value: "regular",
},
{
label: "Compact",
value: "compact",
},
],
defaultValue: "regular",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
],
},
];

View File

@ -0,0 +1,12 @@
export const settersConfig = {
__setters: {
setVisibility: {
path: "isVisible",
type: "boolean",
},
setDisabled: {
path: "isDisabled",
type: "boolean",
},
},
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" d="m3.5 7.5 1 1 1-2M9.5 8.5l2-2m0 2-2-2"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" d="M12.5 3.5h-10a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2Z"/></svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@ -0,0 +1,3 @@
import { WDSToolbarButtonsWidget } from "./widget";
export { WDSToolbarButtonsWidget };

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="76" fill="none"><rect width="59" height="23" x="6.5" y="26.5" fill="#fff" rx="4.5"/><rect width="59" height="23" x="6.5" y="26.5" stroke="#CDD5DF" rx="4.5"/><rect width="24" height="15" x="10.5" y="30.5" fill="#FFEEE5" rx="2.5"/><rect width="24" height="15" x="10.5" y="30.5" stroke="#CC3D00" rx="2.5"/><path stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round" d="m18.5 37.5 3 3 5-5"/><rect width="24" height="15" x="37.5" y="30.5" fill="#FFEEE5" rx="2.5"/><rect width="24" height="15" x="37.5" y="30.5" stroke="#CC3D00" rx="2.5"/><path stroke="#CC3D00" stroke-linecap="round" d="m46.5 35 6 6m0-6-6 6"/></svg>

After

Width:  |  Height:  |  Size: 680 B

View File

@ -12,12 +12,12 @@ import {
anvilConfig,
} from "./../config";
import type { ButtonGroupWidgetProps } from "./types";
import { ButtonGroupComponent } from "../component";
import { ToolbarButtonsComponent } from "../component";
import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionConstants";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import type { AnvilConfig } from "WidgetProvider/constants";
class WDSButtonGroupWidget extends BaseWidget<
class WDSToolbarButtonsWidget extends BaseWidget<
ButtonGroupWidgetProps,
WidgetState
> {
@ -25,7 +25,7 @@ class WDSButtonGroupWidget extends BaseWidget<
super(props);
}
static type = "WDS_BUTTON_GROUP_WIDGET";
static type = "WDS_TOOLBAR_BUTTONS_WIDGET";
static getConfig() {
return metaConfig;
@ -75,7 +75,7 @@ class WDSButtonGroupWidget extends BaseWidget<
getWidgetView() {
return (
<ButtonGroupComponent
<ToolbarButtonsComponent
buttonsList={this.props.buttonsList}
color={this.props.buttonColor}
density={this.props.density}
@ -88,4 +88,4 @@ class WDSButtonGroupWidget extends BaseWidget<
}
}
export { WDSButtonGroupWidget };
export { WDSToolbarButtonsWidget };

View File

@ -0,0 +1,13 @@
import type { ButtonGroupProps } from "@design-system/widgets";
import type { WidgetProps } from "widgets/BaseWidget";
import type { ToolbarButtonsItemComponentProps } from "../component/types";
export type ButtonsList = Record<string, ToolbarButtonsItemComponentProps>;
export interface ButtonGroupWidgetProps extends WidgetProps {
buttonColor: ButtonGroupProps<object>["color"];
buttonVariant: ButtonGroupProps<object>["variant"];
orientation: ButtonGroupProps<object>["orientation"];
isVisible: boolean;
buttonsList: ButtonsList;
}

View File

@ -10,7 +10,7 @@ export const WDS_V2_WIDGET_MAP = {
TEXT_WIDGET_V2: "WDS_HEADING_WIDGET",
TABLE_WIDGET_V2: "WDS_TABLE_WIDGET",
CURRENCY_INPUT_WIDGET: "WDS_CURRENCY_INPUT_WIDGET",
BUTTON_GROUP_WIDGET: "WDS_BUTTON_GROUP_WIDGET",
BUTTON_GROUP_WIDGET: "WDS_TOOLBAR_BUTTONS_WIDGET",
PHONE_INPUT_WIDGET: "WDS_PHONE_INPUT_WIDGET",
CHECKBOX_GROUP_WIDGET: "WDS_CHECKBOX_GROUP_WIDGET",
SWITCH_WIDGET: "WDS_SWITCH_WIDGET",
@ -20,6 +20,7 @@ export const WDS_V2_WIDGET_MAP = {
MODAL_WIDGET: "WDS_MODAL_WIDGET",
STATBOX_WIDGET: "WDS_STATBOX_WIDGET",
KEY_VALUE_WIDGET: "WDS_KEY_VALUE_WIDGET",
INLINE_BUTTONS_WIDGET: "WDS_INLINE_BUTTONS_WIDGET",
// Anvil layout widgets
ZONE_WIDGET: anvilWidgets.ZONE_WIDGET,

View File

@ -65,6 +65,10 @@ export const getPlatformFunctions = () => {
return platformFns;
};
export const getEntityFunctions = () => {
return entityFns;
};
const platformFns = [
{
name: "navigateTo",
@ -112,7 +116,7 @@ const platformFns = [
},
];
export const entityFns = [
const entityFns = [
{
name: "run",
qualifier: (entity: DataTreeEntity) => isRunNClearFnQualifierEntity(entity),

View File

@ -35,5 +35,5 @@
"importsNotUsedAsValues": "error"
},
"include": ["./src/**/*", "./packages"],
"exclude": ["./packages/rts"]
"exclude": ["./packages/rts", "**/node_modules", "**/.*/"]
}

View File

@ -21441,9 +21441,9 @@ __metadata:
linkType: hard
"ip@npm:^2.0.0":
version: 2.0.0
resolution: "ip@npm:2.0.0"
checksum: cfcfac6b873b701996d71ec82a7dd27ba92450afdb421e356f44044ed688df04567344c36cbacea7d01b1c39a4c732dc012570ebe9bebfb06f27314bca625349
version: 2.0.1
resolution: "ip@npm:2.0.1"
checksum: d765c9fd212b8a99023a4cde6a558a054c298d640fec1020567494d257afd78ca77e37126b1a3ef0e053646ced79a816bf50621d38d5e768cdde0431fa3b0d35
languageName: node
linkType: hard

View File

@ -239,6 +239,13 @@
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core-micrometer</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.74</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,66 @@
package com.appsmith.external.helpers;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.util.Base64;
public class RSAKeyUtil {
static {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
}
private static final String PKCS_1_PEM_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
private static final String PKCS_1_PEM_FOOTER = "-----END RSA PRIVATE KEY-----";
private static final String PKCS_8_PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
private static final String PKCS_8_PEM_FOOTER = "-----END PRIVATE KEY-----";
public static boolean isPkcs1Key(String key) {
return key.contains(PKCS_1_PEM_HEADER) && key.contains(PKCS_1_PEM_FOOTER);
}
public static String replaceHeaderAndFooterFromKey(String key) {
return key.replace(PKCS_1_PEM_HEADER, "")
.replace(PKCS_1_PEM_FOOTER, "")
.replace(PKCS_8_PEM_HEADER, "")
.replace(PKCS_8_PEM_FOOTER, "")
.replace("\n", "");
}
public static PrivateKey readPkcs8PrivateKey(byte[] pkcs8Bytes) throws GeneralSecurityException {
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "SunRsaSign");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8Bytes);
try {
return keyFactory.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException("Unexpected key format! Accepted key formats are pkcs8, pkcs1", e);
}
}
public static PrivateKey readPkcs1PrivateKey(String base64EncodedPKCS1) throws Exception {
// Decode the Base64-encoded PKCS#1 key
byte[] pkcs1KeyBytes = Base64.getDecoder().decode(base64EncodedPKCS1);
// Use Bouncy Castle's ASN1 classes to parse the ASN.1 structure of the PKCS#1 key
ASN1InputStream asn1InputStream = new ASN1InputStream(pkcs1KeyBytes);
RSAPrivateKey rsaPrivateKey = RSAPrivateKey.getInstance(asn1InputStream.readObject());
asn1InputStream.close();
// Convert ASN.1 structure to RSAPrivateKeySpec
RSAPrivateKeySpec rsaPrivateKeySpec =
new RSAPrivateKeySpec(rsaPrivateKey.getModulus(), rsaPrivateKey.getPrivateExponent());
// Generate the PrivateKey object
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(rsaPrivateKeySpec);
}
}

View File

@ -59,6 +59,7 @@ public class SSLDetails implements AppsmithDomain {
AuthType authType;
// For Mutual TLS of datasource integration
CACertificateType caCertificateType;
UploadedFile keyFile;

View File

@ -30,6 +30,7 @@ import com.appsmith.external.services.SharedConfig;
import com.external.plugins.datatypes.PostgresSpecificDataTypes;
import com.external.plugins.exceptions.PostgresErrorMessages;
import com.external.plugins.exceptions.PostgresPluginError;
import com.external.plugins.utils.MutualTLSCertValidatingFactory;
import com.external.plugins.utils.PostgresDatasourceUtils;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@ -87,6 +88,7 @@ import static com.appsmith.external.helpers.PluginUtils.getIdenticalColumns;
import static com.appsmith.external.helpers.PluginUtils.getPSParamLabel;
import static com.appsmith.external.helpers.Sizeof.sizeof;
import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuestionMarkWithDollarIndex;
import static com.appsmith.external.models.SSLDetails.AuthType.VERIFY_CA;
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.BOOL;
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.DATE;
import static com.external.plugins.utils.PostgresDataTypeUtils.DataType.DECIMAL;
@ -1183,8 +1185,42 @@ public class PostgresPlugin extends BasePlugin {
break;
case DEFAULT:
/* do nothing - accept default driver setting */
break;
case VERIFY_CA:
case VERIFY_FULL:
config.addDataSourceProperty("ssl", "true");
if (sslAuthType == SSLDetails.AuthType.VERIFY_FULL) {
config.addDataSourceProperty("sslmode", "verify-full");
} else {
config.addDataSourceProperty("sslmode", "verify-ca");
}
// Common properties for both VERIFY_CA and VERIFY_FULL
config.addDataSourceProperty("sslfactory", MutualTLSCertValidatingFactory.class.getName());
config.addDataSourceProperty(
"clientCertString",
datasourceConfiguration
.getConnection()
.getSsl()
.getCertificateFile()
.getBase64Content());
config.addDataSourceProperty(
"clientKeyString",
datasourceConfiguration
.getConnection()
.getSsl()
.getKeyFile()
.getBase64Content());
config.addDataSourceProperty(
"serverCACertString",
datasourceConfiguration
.getConnection()
.getSsl()
.getCaCertificateFile()
.getBase64Content());
break;
default:
throw new AppsmithPluginException(
PostgresPluginError.POSTGRES_PLUGIN_ERROR,

View File

@ -0,0 +1,70 @@
package com.external.plugins.utils;
import com.appsmith.external.helpers.RSAKeyUtil;
import org.postgresql.ssl.WrappedFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Properties;
public class MutualTLSCertValidatingFactory extends WrappedFactory {
public MutualTLSCertValidatingFactory(Properties info) throws Exception {
// Convert String certificates and keys to objects
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// Client certificate
ByteArrayInputStream clientCertIS =
new ByteArrayInputStream(Base64.getDecoder().decode(info.getProperty("clientCertString")));
X509Certificate clientCertificate = (X509Certificate) cf.generateCertificate(clientCertIS);
// Client key and this assumes we are using RSA keys
PrivateKey privateKey;
if (RSAKeyUtil.isPkcs1Key(info.getProperty("clientKeyString"))) {
String rsaKey = RSAKeyUtil.replaceHeaderAndFooterFromKey(info.getProperty("clientKeyString"));
privateKey = RSAKeyUtil.readPkcs1PrivateKey(rsaKey);
} else {
String rsaKey = RSAKeyUtil.replaceHeaderAndFooterFromKey(info.getProperty("clientKeyString"));
privateKey = RSAKeyUtil.readPkcs8PrivateKey(Base64.getDecoder().decode(rsaKey));
}
// CA certificate for verifying the server
ByteArrayInputStream caCertIS =
new ByteArrayInputStream(Base64.getDecoder().decode(info.getProperty("serverCACertString")));
X509Certificate caCertificate = (X509Certificate) cf.generateCertificate(caCertIS);
// Client keystore
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("client-cert", clientCertificate);
keyStore.setKeyEntry("client-key", privateKey, "password".toCharArray(), new java.security.cert.Certificate[] {
clientCertificate
});
// Truststore
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
trustStore.setCertificateEntry("ca-cert", caCertificate);
// Initialize SSLContext
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "password".toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
this.factory = sslContext.getSocketFactory();
}
}

View File

@ -1,63 +1,55 @@
package com.appsmith.server.helpers.ce.bridge;
import com.appsmith.external.models.BaseDomain;
import lombok.NonNull;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.query.Criteria;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Bridge extends Criteria {
private final List<Criteria> criteriaList = new ArrayList<>();
public final class Bridge {
private Bridge() {}
public static Bridge bridge() {
return new Bridge();
public static <T extends BaseDomain> BridgeQuery<T> query() {
return new BridgeQuery<>();
}
public Bridge equal(@NonNull String key, @NonNull String value) {
criteriaList.add(Criteria.where(key).is(value));
return this;
@Deprecated
public static <T extends BaseDomain> BridgeQuery<T> bridge() {
return new BridgeQuery<>();
}
public Bridge equal(@NonNull String key, @NonNull ObjectId value) {
criteriaList.add(Criteria.where(key).is(value));
return this;
@SafeVarargs
public static <T extends BaseDomain> BridgeQuery<T> or(BridgeQuery<T>... items) {
final BridgeQuery<T> q = new BridgeQuery<>();
q.checks.add(new Criteria().orOperator(items));
return q;
}
public Bridge in(@NonNull String key, @NonNull Collection<String> value) {
criteriaList.add(Criteria.where(key).in(value));
return this;
@SafeVarargs
public static <T extends BaseDomain> BridgeQuery<T> and(BridgeQuery<T>... items) {
final BridgeQuery<T> q = new BridgeQuery<>();
q.checks.add(new Criteria().andOperator(items));
return q;
}
public Bridge exists(@NonNull String key) {
criteriaList.add(Criteria.where(key).exists(true));
return this;
public static <T extends BaseDomain> BridgeQuery<T> equal(@NonNull String key, @NonNull String value) {
return Bridge.<T>query().equal(key, value);
}
public Bridge isTrue(@NonNull String key) {
criteriaList.add(Criteria.where(key).is(true));
return this;
public static <T extends BaseDomain> BridgeQuery<T> equal(@NonNull String key, @NonNull ObjectId value) {
return Bridge.<T>query().equal(key, value);
}
/**
* Explicitly disable the `where()` API to prevent its usage. This is because querying with this API will work here,
* but won't work in the Postgres version.
*/
public static Bridge where() {
throw new UnsupportedOperationException("Not implemented");
public static <T extends BaseDomain> BridgeQuery<T> in(@NonNull String key, @NonNull Collection<String> value) {
return Bridge.<T>query().in(key, value);
}
@Override
public Document getCriteriaObject() {
if (criteriaList.isEmpty()) {
throw new UnsupportedOperationException(
"Empty bridge criteria leads to subtle bugs. Just don't call `.criteria()` in such cases.");
}
public static <T extends BaseDomain> BridgeQuery<T> exists(@NonNull String key) {
return Bridge.<T>query().exists(key);
}
return new Criteria().andOperator(criteriaList.toArray(new Criteria[0])).getCriteriaObject();
public static <T extends BaseDomain> BridgeQuery<T> isTrue(@NonNull String key) {
return Bridge.<T>query().isTrue(key);
}
}

View File

@ -0,0 +1,70 @@
package com.appsmith.server.helpers.ce.bridge;
import com.appsmith.external.models.BaseDomain;
import lombok.NonNull;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.query.Criteria;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public final class BridgeQuery<T extends BaseDomain> extends Criteria {
final List<Criteria> checks = new ArrayList<>();
BridgeQuery() {}
public BridgeQuery<T> equal(@NonNull String key, @NonNull String value) {
checks.add(Criteria.where(key).is(value));
return this;
}
public BridgeQuery<T> equal(@NonNull String key, @NonNull ObjectId value) {
checks.add(Criteria.where(key).is(value));
return this;
}
public BridgeQuery<T> in(@NonNull String key, @NonNull Collection<String> value) {
checks.add(Criteria.where(key).in(value));
return this;
}
public BridgeQuery<T> exists(@NonNull String key) {
checks.add(Criteria.where(key).exists(true));
return this;
}
public BridgeQuery<T> isTrue(@NonNull String key) {
checks.add(Criteria.where(key).is(true));
return this;
}
public BridgeQuery<T> or(BridgeQuery... items) {
checks.add(new Criteria().orOperator(items));
return this;
}
public BridgeQuery<T> and(BridgeQuery... items) {
checks.add(new Criteria().andOperator(items));
return this;
}
/**
* Explicitly disable the `where()` API to prevent its usage. This is because querying with this API will work here,
* but won't work in the Postgres version.
*/
public static Bridge where() {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public Document getCriteriaObject() {
if (checks.isEmpty()) {
throw new UnsupportedOperationException(
"Empty bridge criteria leads to subtle bugs. Just don't call `.criteria()` in such cases.");
}
return new Criteria().andOperator(checks.toArray(new Criteria[0])).getCriteriaObject();
}
}

View File

@ -12,6 +12,7 @@ import com.appsmith.server.dtos.WorkspacePluginStatus;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.ce.bridge.Bridge;
import com.appsmith.server.helpers.ce.bridge.BridgeQuery;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.BaseService;
@ -55,8 +56,6 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static com.appsmith.server.helpers.ce.bridge.Bridge.bridge;
@Slf4j
public class PluginServiceCEImpl extends BaseService<PluginRepository, Plugin, String> implements PluginServiceCE {
@ -131,7 +130,7 @@ public class PluginServiceCEImpl extends BaseService<PluginRepository, Plugin, S
List<String> pluginIds = org.getPlugins().stream()
.map(WorkspacePlugin::getPluginId)
.collect(Collectors.toList());
final Bridge criteria = bridge().in(FieldName.ID, pluginIds);
final BridgeQuery<Plugin> criteria = Bridge.in(FieldName.ID, pluginIds);
final String typeString = params.getFirst(FieldName.TYPE);
if (typeString != null) {

View File

@ -5,6 +5,7 @@ import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.User;
import com.appsmith.server.helpers.ce.bridge.Bridge;
import com.appsmith.server.repositories.BaseAppsmithRepositoryImpl;
import com.appsmith.server.repositories.CacheableRepositoryHelper;
import com.appsmith.server.solutions.ApplicationPermission;
@ -25,7 +26,6 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import static com.appsmith.server.helpers.ce.bridge.Bridge.bridge;
import static org.springframework.data.mongodb.core.query.Criteria.where;
@Slf4j
@ -55,7 +55,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
public Mono<Application> findByIdAndWorkspaceId(String id, String workspaceId, AclPermission permission) {
return queryBuilder()
.byId(id)
.criteria(bridge().equal(Application.Fields.workspaceId, workspaceId))
.criteria(Bridge.equal(Application.Fields.workspaceId, workspaceId))
.permission(permission)
.one();
}
@ -63,7 +63,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
@Override
public Mono<Application> findByName(String name, AclPermission permission) {
return queryBuilder()
.criteria(bridge().equal(Application.Fields.name, name))
.criteria(Bridge.equal(Application.Fields.name, name))
.permission(permission)
.one();
}
@ -71,7 +71,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
@Override
public Flux<Application> findByWorkspaceId(String workspaceId, AclPermission permission) {
return queryBuilder()
.criteria(bridge().equal(Application.Fields.workspaceId, workspaceId))
.criteria(Bridge.equal(Application.Fields.workspaceId, workspaceId))
.permission(permission)
.all();
}
@ -111,7 +111,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
@Override
public Flux<Application> findByClonedFromApplicationId(String applicationId, AclPermission permission) {
return queryBuilder()
.criteria(bridge().equal(Application.Fields.clonedFromApplicationId, applicationId))
.criteria(Bridge.equal(Application.Fields.clonedFromApplicationId, applicationId))
.permission(permission)
.all();
}
@ -140,12 +140,12 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
final Mono<Integer> setAllAsNonDefaultMono = queryBuilder()
.byId(applicationId)
.criteria(bridge().isTrue("pages.isDefault"))
.criteria(Bridge.isTrue("pages.isDefault"))
.updateFirst(new Update().set("pages.$.isDefault", false));
final Mono<Integer> setDefaultMono = queryBuilder()
.byId(applicationId)
.criteria(bridge().equal("pages._id", new ObjectId(pageId)))
.criteria(Bridge.equal("pages._id", new ObjectId(pageId)))
.updateFirst(new Update().set("pages.$.isDefault", true));
return setAllAsNonDefaultMono.then(setDefaultMono).then();
@ -166,7 +166,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
AclPermission aclPermission) {
return queryBuilder()
.criteria(bridge().equal(
.criteria(Bridge.equal(
Application.Fields.gitApplicationMetadata_defaultApplicationId, defaultApplicationId)
.equal(Application.Fields.gitApplicationMetadata_branchName, branchName))
.fields(projectionFieldNames)
@ -179,7 +179,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
String defaultApplicationId, String branchName, Optional<AclPermission> aclPermission) {
return queryBuilder()
.criteria(bridge().equal(
.criteria(Bridge.equal(
Application.Fields.gitApplicationMetadata_defaultApplicationId, defaultApplicationId)
.equal(Application.Fields.gitApplicationMetadata_branchName, branchName))
.permission(aclPermission.orElse(null))
@ -191,8 +191,8 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
String defaultApplicationId, AclPermission permission) {
return queryBuilder()
.criteria(bridge().equal(
Application.Fields.gitApplicationMetadata_defaultApplicationId, defaultApplicationId))
.criteria(Bridge.equal(
Application.Fields.gitApplicationMetadata_defaultApplicationId, defaultApplicationId))
.permission(permission)
.all();
}
@ -200,14 +200,14 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
@Override
public Mono<Long> countByWorkspaceId(String workspaceId) {
return queryBuilder()
.criteria(bridge().equal(Application.Fields.workspaceId, workspaceId))
.criteria(Bridge.equal(Application.Fields.workspaceId, workspaceId))
.count();
}
@Override
public Mono<Long> getGitConnectedApplicationWithPrivateRepoCount(String workspaceId) {
return queryBuilder()
.criteria(bridge().equal(Application.Fields.workspaceId, workspaceId)
.criteria(Bridge.equal(Application.Fields.workspaceId, workspaceId)
.isTrue(Application.Fields.gitApplicationMetadata_isRepoPrivate))
.count();
}
@ -216,7 +216,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
public Flux<Application> getGitConnectedApplicationByWorkspaceId(String workspaceId) {
AclPermission aclPermission = applicationPermission.getEditPermission();
return queryBuilder()
.criteria(bridge()
.criteria(Bridge
// isRepoPrivate and gitAuth will be stored only with default application which ensures we will
// have only single application per repo
.exists(Application.Fields.gitApplicationMetadata_isRepoPrivate)
@ -230,8 +230,8 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
public Mono<Application> getApplicationByDefaultApplicationIdAndDefaultBranch(String defaultApplicationId) {
return queryBuilder()
.criteria(bridge().equal(
Application.Fields.gitApplicationMetadata_defaultApplicationId, defaultApplicationId))
.criteria(Bridge.equal(
Application.Fields.gitApplicationMetadata_defaultApplicationId, defaultApplicationId))
.one();
}
@ -252,7 +252,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
@Override
public Mono<Long> countByNameAndWorkspaceId(String applicationName, String workspaceId, AclPermission permission) {
return queryBuilder()
.criteria(bridge().equal(Application.Fields.workspaceId, workspaceId)
.criteria(Bridge.equal(Application.Fields.workspaceId, workspaceId)
.equal(Application.Fields.name, applicationName))
.permission(permission)
.count();
@ -298,7 +298,7 @@ public class CustomApplicationRepositoryCEImpl extends BaseAppsmithRepositoryImp
Update unsetProtected = new Update().set(isProtectedFieldPath, false);
return queryBuilder()
.criteria(bridge().equal(Application.Fields.gitApplicationMetadata_defaultApplicationId, applicationId))
.criteria(Bridge.equal(Application.Fields.gitApplicationMetadata_defaultApplicationId, applicationId))
.permission(permission)
.updateAll(unsetProtected);
}

View File

@ -3,6 +3,7 @@ package com.appsmith.server.repositories.ce;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Config;
import com.appsmith.server.domains.User;
import com.appsmith.server.helpers.ce.bridge.Bridge;
import com.appsmith.server.repositories.BaseAppsmithRepositoryImpl;
import com.appsmith.server.repositories.CacheableRepositoryHelper;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@ -10,7 +11,6 @@ import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Criteria;
import reactor.core.publisher.Mono;
import static com.appsmith.server.helpers.ce.bridge.Bridge.bridge;
import static org.springframework.data.mongodb.core.query.Criteria.where;
public class CustomConfigRepositoryCEImpl extends BaseAppsmithRepositoryImpl<Config>
@ -32,7 +32,7 @@ public class CustomConfigRepositoryCEImpl extends BaseAppsmithRepositoryImpl<Con
@Override
public Mono<Config> findByNameAsUser(String name, User user, AclPermission permission) {
return getAllPermissionGroupsForUser(user).flatMap(permissionGroups -> queryBuilder()
.criteria(bridge().equal(Config.Fields.name, name))
.criteria(Bridge.equal(Config.Fields.name, name))
.permission(permission)
.permissionGroups(permissionGroups)
.one());

Some files were not shown because too many files have changed in this diff Show More