chore: Add button v2 under feature flag (#25106)
This commit is contained in:
parent
7c75100e58
commit
2fd0f6f3c2
|
|
@ -31,6 +31,9 @@ module.exports = {
|
||||||
"design-system-old": "<rootDir>/node_modules/design-system-old/build",
|
"design-system-old": "<rootDir>/node_modules/design-system-old/build",
|
||||||
"@design-system/widgets-old":
|
"@design-system/widgets-old":
|
||||||
"<rootDir>/node_modules/@design-system/widgets-old",
|
"<rootDir>/node_modules/@design-system/widgets-old",
|
||||||
|
"@design-system/widgets": "<rootDir>/node_modules/@design-system/widgets",
|
||||||
|
"@design-system/headless": "<rootDir>/node_modules/@design-system/headless",
|
||||||
|
"@design-system/theming": "<rootDir>/node_modules/@design-system/theming",
|
||||||
"design-system": "<rootDir>/node_modules/design-system/build",
|
"design-system": "<rootDir>/node_modules/design-system/build",
|
||||||
"^proxy-memoize$": "<rootDir>/node_modules/proxy-memoize/dist/wrapper.cjs",
|
"^proxy-memoize$": "<rootDir>/node_modules/proxy-memoize/dist/wrapper.cjs",
|
||||||
// @blueprintjs packages need to be resolved to the `esnext` directory. The default `esm` directory
|
// @blueprintjs packages need to be resolved to the `esnext` directory. The default `esm` directory
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||||
|
|
||||||
import { Tooltip, TooltipTrigger, TooltipContent } from "./";
|
import { TooltipRoot, TooltipTrigger, TooltipContent } from "./";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
|
|
||||||
<Meta
|
<Meta
|
||||||
title="Design-system/headless/Tooltip"
|
title="Design-system/headless/Tooltip"
|
||||||
component={Tooltip}
|
component={TooltipRoot}
|
||||||
args={{
|
args={{
|
||||||
open: undefined,
|
open: undefined,
|
||||||
onOpenChange: undefined,
|
onOpenChange: undefined,
|
||||||
|
|
@ -14,12 +14,12 @@ import { Button } from "../Button";
|
||||||
|
|
||||||
export const Template = (args) => {
|
export const Template = (args) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip {...args}>
|
<TooltipRoot {...args}>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<button>My trigger</button>
|
<button>My trigger</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -37,30 +37,30 @@ The placement of the tooltip can be changed by passing the `placement` prop.
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Tooltip placement">
|
<Story name="Tooltip placement">
|
||||||
<Tooltip placement="left">
|
<TooltipRoot placement="left">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<button>Left</button>
|
<button>Left</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
<Tooltip placement="top">
|
<TooltipRoot placement="top">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<button>Top</button>
|
<button>Top</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
<Tooltip placement="bottom">
|
<TooltipRoot placement="bottom">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<button>Bottom</button>
|
<button>Bottom</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
<Tooltip placement="right">
|
<TooltipRoot placement="right">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<button>Right</button>
|
<button>Right</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
|
@ -70,12 +70,12 @@ If the trigger is disabled, the tooltip will not be displayed.
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Tooltip disabled">
|
<Story name="Tooltip disabled">
|
||||||
<Tooltip>
|
<TooltipRoot>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button isDisabled>Disabled</Button>
|
<Button isDisabled>Disabled</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent disabled>My tooltip</TooltipContent>
|
<TooltipContent disabled>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import { useTooltip } from "./useTooltip";
|
||||||
import { TooltipContext } from "./TooltipContext";
|
import { TooltipContext } from "./TooltipContext";
|
||||||
import type { TooltipOptions } from "./useTooltip";
|
import type { TooltipOptions } from "./useTooltip";
|
||||||
|
|
||||||
type TooltipProps = { children: ReactNode } & TooltipOptions;
|
type TooltipRootProps = { children: ReactNode } & TooltipOptions;
|
||||||
|
|
||||||
export function Tooltip({ children, ...options }: TooltipProps) {
|
export function TooltipRoot({ children, ...options }: TooltipRootProps) {
|
||||||
const tooltip = useTooltip(options);
|
const tooltip = useTooltip(options);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export { Tooltip } from "./Tooltip";
|
export { TooltipRoot } from "./TooltipRoot";
|
||||||
export { TooltipTrigger } from "./TooltipTrigger";
|
export { TooltipTrigger } from "./TooltipTrigger";
|
||||||
export { TooltipContent } from "./TooltipContent";
|
export { TooltipContent } from "./TooltipContent";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./ThemeContext";
|
export * from "./ThemeContext";
|
||||||
export * from "./ThemeProvider";
|
export * from "./ThemeProvider";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
export * from "./useTheme";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import type { ColorMode } from "../color";
|
||||||
|
import type { FontFamily } from "../typography";
|
||||||
import type { RootUnit, ThemeToken } from "../token";
|
import type { RootUnit, ThemeToken } from "../token";
|
||||||
|
|
||||||
export type Theme = ThemeToken & {
|
export type Theme = ThemeToken & {
|
||||||
|
|
@ -15,3 +18,11 @@ export interface ThemeProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UseThemeProps = {
|
||||||
|
seedColor?: string;
|
||||||
|
colorMode?: ColorMode;
|
||||||
|
borderRadius?: string;
|
||||||
|
fontFamily?: FontFamily;
|
||||||
|
rootUnitRatio?: number;
|
||||||
|
};
|
||||||
|
|
|
||||||
137
app/client/packages/design-system/theming/src/theme/useTheme.tsx
Normal file
137
app/client/packages/design-system/theming/src/theme/useTheme.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
import Color from "colorjs.io";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
TokensAccessor,
|
||||||
|
defaultTokens,
|
||||||
|
useFluidTokens,
|
||||||
|
} from "@design-system/theming";
|
||||||
|
import type { ColorMode, FontFamily } from "@design-system/theming";
|
||||||
|
|
||||||
|
import type { UseThemeProps } from "./types";
|
||||||
|
|
||||||
|
export function useTheme(props: UseThemeProps) {
|
||||||
|
const {
|
||||||
|
borderRadius,
|
||||||
|
colorMode = "light",
|
||||||
|
fontFamily,
|
||||||
|
rootUnitRatio: rootUnitRatioProp,
|
||||||
|
seedColor,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [rootUnitRatio, setRootUnitRatio] = useState(1);
|
||||||
|
const { fluid, ...restDefaultTokens } = defaultTokens;
|
||||||
|
|
||||||
|
const { rootUnit, sizing, spacing, typography } = useFluidTokens(
|
||||||
|
fluid,
|
||||||
|
rootUnitRatio,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokensAccessor = new TokensAccessor({
|
||||||
|
...restDefaultTokens,
|
||||||
|
rootUnit,
|
||||||
|
spacing,
|
||||||
|
sizing,
|
||||||
|
typography,
|
||||||
|
colorMode: colorMode as ColorMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [theme, setTheme] = useState(tokensAccessor.getAllTokens());
|
||||||
|
|
||||||
|
const updateFontFamily = (fontFamily: FontFamily) => {
|
||||||
|
tokensAccessor.updateFontFamily(fontFamily);
|
||||||
|
|
||||||
|
setTheme((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
typography: tokensAccessor.getTypography(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (colorMode) {
|
||||||
|
tokensAccessor.updateColorMode(colorMode);
|
||||||
|
|
||||||
|
setTheme((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
...tokensAccessor.getColors(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [colorMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (borderRadius) {
|
||||||
|
tokensAccessor.updateBorderRadius({
|
||||||
|
1: borderRadius,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTheme((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
...tokensAccessor.getBorderRadius(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [borderRadius]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (seedColor) {
|
||||||
|
let color;
|
||||||
|
|
||||||
|
try {
|
||||||
|
color = Color.parse(seedColor);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
tokensAccessor.updateSeedColor(seedColor);
|
||||||
|
|
||||||
|
setTheme((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
...tokensAccessor.getColors(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [seedColor]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fontFamily) {
|
||||||
|
updateFontFamily(fontFamily);
|
||||||
|
}
|
||||||
|
}, [fontFamily]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rootUnitRatioProp) {
|
||||||
|
setRootUnitRatio(rootUnitRatioProp);
|
||||||
|
tokensAccessor.updateRootUnit(rootUnit);
|
||||||
|
tokensAccessor.updateSpacing(spacing);
|
||||||
|
|
||||||
|
setTheme((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
rootUnit: tokensAccessor.getRootUnit(),
|
||||||
|
...tokensAccessor.getSpacing(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [rootUnitRatioProp]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tokensAccessor.updateTypography(typography);
|
||||||
|
|
||||||
|
setTheme((prevState) => {
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
typography: tokensAccessor.getTypography(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [typography]);
|
||||||
|
|
||||||
|
return { theme, setTheme };
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ export class TokensAccessor {
|
||||||
private fontFamily?: FontFamily;
|
private fontFamily?: FontFamily;
|
||||||
private spacing?: TokenObj;
|
private spacing?: TokenObj;
|
||||||
private sizing?: TokenObj;
|
private sizing?: TokenObj;
|
||||||
|
private zIndex?: TokenObj;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
borderRadius,
|
borderRadius,
|
||||||
|
|
@ -37,6 +38,7 @@ export class TokensAccessor {
|
||||||
sizing,
|
sizing,
|
||||||
spacing,
|
spacing,
|
||||||
typography,
|
typography,
|
||||||
|
zIndex,
|
||||||
}: TokenSource) {
|
}: TokenSource) {
|
||||||
this.seedColor = seedColor;
|
this.seedColor = seedColor;
|
||||||
this.colorMode = colorMode;
|
this.colorMode = colorMode;
|
||||||
|
|
@ -49,6 +51,7 @@ export class TokensAccessor {
|
||||||
this.sizing = sizing;
|
this.sizing = sizing;
|
||||||
this.spacing = spacing;
|
this.spacing = spacing;
|
||||||
this.typography = typography;
|
this.typography = typography;
|
||||||
|
this.zIndex = zIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRootUnit = (rootUnit: RootUnit) => {
|
updateRootUnit = (rootUnit: RootUnit) => {
|
||||||
|
|
@ -87,6 +90,10 @@ export class TokensAccessor {
|
||||||
this.opacity = opacity;
|
this.opacity = opacity;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateZIndex = (zIndex: TokenObj) => {
|
||||||
|
this.zIndex = zIndex;
|
||||||
|
};
|
||||||
|
|
||||||
updateSpacing = (spacing: TokenObj) => {
|
updateSpacing = (spacing: TokenObj) => {
|
||||||
this.spacing = spacing;
|
this.spacing = spacing;
|
||||||
};
|
};
|
||||||
|
|
@ -107,6 +114,7 @@ export class TokensAccessor {
|
||||||
...this.getBoxShadow(),
|
...this.getBoxShadow(),
|
||||||
...this.getBorderWidth(),
|
...this.getBorderWidth(),
|
||||||
...this.getOpacity(),
|
...this.getOpacity(),
|
||||||
|
...this.getZIndex(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -178,6 +186,12 @@ export class TokensAccessor {
|
||||||
return this.createTokenObject(this.opacity, "opacity");
|
return this.createTokenObject(this.opacity, "opacity");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getZIndex = () => {
|
||||||
|
if (this.zIndex == null) return {} as ThemeToken;
|
||||||
|
|
||||||
|
return this.createTokenObject(this.zIndex, "zIndex");
|
||||||
|
};
|
||||||
|
|
||||||
private get isLightMode() {
|
private get isLightMode() {
|
||||||
return this.colorMode === "light";
|
return this.colorMode === "light";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,5 +47,11 @@
|
||||||
},
|
},
|
||||||
"opacity": {
|
"opacity": {
|
||||||
"disabled": 0.3
|
"disabled": 0.3
|
||||||
|
},
|
||||||
|
"zIndex": {
|
||||||
|
"1": 3,
|
||||||
|
"2": 4,
|
||||||
|
"3": 10,
|
||||||
|
"99": 9999
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ export type TokenType =
|
||||||
| "borderRadius"
|
| "borderRadius"
|
||||||
| "boxShadow"
|
| "boxShadow"
|
||||||
| "borderWidth"
|
| "borderWidth"
|
||||||
| "opacity";
|
| "opacity"
|
||||||
|
| "zIndex";
|
||||||
|
|
||||||
export interface Token {
|
export interface Token {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
|
|
@ -31,6 +32,7 @@ export interface TokenSource {
|
||||||
borderWidth?: TokenObj;
|
borderWidth?: TokenObj;
|
||||||
opacity?: TokenObj;
|
opacity?: TokenObj;
|
||||||
fontFamily?: FontFamily;
|
fontFamily?: FontFamily;
|
||||||
|
zIndex?: TokenObj;
|
||||||
sizing?: TokenObj;
|
sizing?: TokenObj;
|
||||||
spacing?: TokenObj;
|
spacing?: TokenObj;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useVisuallyHidden } from "@react-aria/visually-hidden";
|
||||||
|
|
||||||
import { Text } from "../Text";
|
import { Text } from "../Text";
|
||||||
import { Spinner } from "../Spinner";
|
import { Spinner } from "../Spinner";
|
||||||
|
import type { ButtonColor, ButtonIconPosition, ButtonVariant } from "./types";
|
||||||
import { DragContainer, StyledButton } from "./index.styled";
|
import { DragContainer, StyledButton } from "./index.styled";
|
||||||
|
|
||||||
export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
|
export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
|
||||||
|
|
@ -15,9 +16,9 @@ export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
|
||||||
*
|
*
|
||||||
* @default "filled"
|
* @default "filled"
|
||||||
*/
|
*/
|
||||||
variant?: "filled" | "outlined" | "ghost";
|
variant?: ButtonVariant;
|
||||||
/** Color tone of the button */
|
/** Color tone of the button */
|
||||||
color?: "accent" | "neutral" | "positive" | "negative" | "warning";
|
color?: ButtonColor;
|
||||||
/** When true, makes the button occupy all the space available */
|
/** When true, makes the button occupy all the space available */
|
||||||
isFitContainer?: boolean;
|
isFitContainer?: boolean;
|
||||||
/** Indicates the loading state of the button */
|
/** Indicates the loading state of the button */
|
||||||
|
|
@ -25,8 +26,8 @@ export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
|
||||||
/** Icon to be used in the button of the button */
|
/** Icon to be used in the button of the button */
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
/** Indicates the position of icon of the button */
|
/** Indicates the position of icon of the button */
|
||||||
iconPosition?: "start" | "end";
|
iconPosition?: ButtonIconPosition;
|
||||||
/** Makes the button visually and functionally disabled but focusable */
|
/** Makes the button visually and functionaly disabled but focusable */
|
||||||
visuallyDisabled?: boolean;
|
visuallyDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +65,9 @@ export const Button = forwardRef(
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{icon}
|
{icon}
|
||||||
<Text lineClamp={1}>{children}</Text>
|
<Text lineClamp={1} textAlign="center">
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -80,7 +83,7 @@ export const Button = forwardRef(
|
||||||
data-button=""
|
data-button=""
|
||||||
data-color={color}
|
data-color={color}
|
||||||
data-fit-container={isFitContainer ? "" : undefined}
|
data-fit-container={isFitContainer ? "" : undefined}
|
||||||
data-icon-position={iconPosition === "start" ? undefined : "end"}
|
data-icon-position={iconPosition === "start" ? "start" : "end"}
|
||||||
data-loading={isLoading ? "" : undefined}
|
data-loading={isLoading ? "" : undefined}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
draggable
|
draggable
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import styled, { css } from "styled-components";
|
import styled, { css } from "styled-components";
|
||||||
import { Button as HeadlessButton } from "@design-system/headless";
|
import { Button as HeadlessButton } from "@design-system/headless";
|
||||||
import type { PickRename } from "../../utils";
|
|
||||||
|
|
||||||
import type { ButtonProps } from "./Button";
|
import type { ButtonProps } from "./Button";
|
||||||
|
import type { PickRename } from "../../utils";
|
||||||
|
|
||||||
type StyledButtonProps = PickRename<
|
type StyledButtonProps = PickRename<
|
||||||
ButtonProps,
|
ButtonProps,
|
||||||
|
|
@ -73,16 +73,19 @@ export const StyledButton = styled(HeadlessButton)<StyledButtonProps>`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
padding: var(--spacing-2) var(--spacing-4);
|
padding: var(--spacing-2) var(--spacing-4);
|
||||||
|
block-size: var(--sizing-8);
|
||||||
border-radius: var(--border-radius-1);
|
border-radius: var(--border-radius-1);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
height: var(--sizing-8);
|
min-inline-size: var(--sizing-8);
|
||||||
min-width: var(--sizing-8);
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
& *:not([data-hidden]) + *:not([data-hidden]) {
|
&[data-icon-position="start"] *:not([data-hidden]) + *:not([data-hidden]) {
|
||||||
margin: 0 var(--spacing-1);
|
margin-inline-start: var(--spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-icon-position="end"] *:not([data-hidden]) + *:not([data-hidden]) {
|
||||||
|
margin-inline-end: var(--spacing-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
${buttonStyles}
|
${buttonStyles}
|
||||||
|
|
@ -96,8 +99,14 @@ export const StyledButton = styled(HeadlessButton)<StyledButtonProps>`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: var(--sizing-5);
|
height: var(--sizing-4);
|
||||||
width: var(--sizing-5);
|
width: var(--sizing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: adding important here as ADS is overriding the color of blueprint icon globally
|
||||||
|
// TODO(pawan): Remove this once ADS team removes the global override
|
||||||
|
&[data-button] .bp3-icon {
|
||||||
|
color: currentColor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export { Button } from "./Button";
|
export { Button } from "./Button";
|
||||||
export type { ButtonProps } from "./Button";
|
export type { ButtonProps } from "./Button";
|
||||||
|
export * from "./types";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
export const BUTTON_VARIANTS = {
|
||||||
|
FILLED: "filled",
|
||||||
|
OUTLINED: "outlined",
|
||||||
|
GHOST: "ghost",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ButtonVariant =
|
||||||
|
(typeof BUTTON_VARIANTS)[keyof typeof BUTTON_VARIANTS];
|
||||||
|
|
||||||
|
export const BUTTON_ICON_POSITIONS = {
|
||||||
|
START: "start",
|
||||||
|
END: "end",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ButtonIconPosition =
|
||||||
|
(typeof BUTTON_ICON_POSITIONS)[keyof typeof BUTTON_ICON_POSITIONS];
|
||||||
|
|
||||||
|
export const BUTTON_COLORS = {
|
||||||
|
ACCENT: "accent",
|
||||||
|
NEUTRAL: "neutral",
|
||||||
|
POSITIVE: "positive",
|
||||||
|
NEGATIVE: "negative",
|
||||||
|
WARNING: "warning",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ButtonColor = (typeof BUTTON_COLORS)[keyof typeof BUTTON_COLORS];
|
||||||
|
|
@ -33,7 +33,7 @@ export const StyledText = styled.div<StyledTextProp>`
|
||||||
font-weight: ${({ $isBold }) => ($isBold ? "bold" : "normal")};
|
font-weight: ${({ $isBold }) => ($isBold ? "bold" : "normal")};
|
||||||
font-style: ${({ $isItalic }) => ($isItalic ? "italic" : "normal")};
|
font-style: ${({ $isItalic }) => ($isItalic ? "italic" : "normal")};
|
||||||
text-align: ${({ $textAlign }) => $textAlign};
|
text-align: ${({ $textAlign }) => $textAlign};
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
color: ${({ color }) => {
|
color: ${({ color }) => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||||
|
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { ButtonGroup } from "../ButtonGroup";
|
import { ButtonGroup } from "../ButtonGroup";
|
||||||
import { Tooltip, TooltipTrigger, TooltipContent } from "./";
|
import { TooltipRoot, TooltipTrigger, TooltipContent } from "./";
|
||||||
|
|
||||||
<Meta
|
<Meta
|
||||||
title="Design-system/widgets/Tooltip"
|
title="Design-system/widgets/Tooltip"
|
||||||
component={Tooltip}
|
component={TooltipRoot}
|
||||||
args={{
|
args={{
|
||||||
open: undefined,
|
open: undefined,
|
||||||
onOpenChange: undefined,
|
onOpenChange: undefined,
|
||||||
|
|
@ -15,12 +15,12 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "./";
|
||||||
|
|
||||||
export const Template = (args) => {
|
export const Template = (args) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip {...args}>
|
<TooltipRoot {...args}>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button>My trigger</Button>
|
<Button>My trigger</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -41,30 +41,30 @@ The placement of the tooltip can be changed by passing the `placement` prop.
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Tooltip placement">
|
<Story name="Tooltip placement">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Tooltip placement="left">
|
<TooltipRoot placement="left">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button variant="outlined">Left</Button>
|
<Button variant="outlined">Left</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
<Tooltip placement="top">
|
<TooltipRoot placement="top">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button variant="outlined">Top</Button>
|
<Button variant="outlined">Top</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
<Tooltip placement="bottom">
|
<TooltipRoot placement="bottom">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button variant="outlined">Bottom</Button>
|
<Button variant="outlined">Bottom</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
<Tooltip placement="right">
|
<TooltipRoot placement="right">
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button variant="outlined">Right</Button>
|
<Button variant="outlined">Right</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
@ -75,7 +75,7 @@ If the trigger is disabled, the tooltip will still be displayed.
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Tooltip disabled">
|
<Story name="Tooltip disabled">
|
||||||
<Tooltip>
|
<TooltipRoot>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button
|
<Button
|
||||||
isDisabled
|
isDisabled
|
||||||
|
|
@ -87,6 +87,16 @@ If the trigger is disabled, the tooltip will still be displayed.
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>My tooltip</TooltipContent>
|
<TooltipContent>My tooltip</TooltipContent>
|
||||||
|
</TooltipRoot>
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
# Tooltip Component
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story name="Tooltip Component">
|
||||||
|
<Tooltip>
|
||||||
|
<Button tooltip="Tooltip">My trigger</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { TooltipRoot, TooltipContent, TooltipTrigger } from "./";
|
||||||
|
|
||||||
|
export type TooltipProps = {
|
||||||
|
tooltip?: React.ReactNode;
|
||||||
|
children: React.ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Tooltip(props: TooltipProps) {
|
||||||
|
const { children, tooltip } = props;
|
||||||
|
|
||||||
|
if (!tooltip) return children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipRoot>
|
||||||
|
<TooltipTrigger>{children}</TooltipTrigger>
|
||||||
|
<TooltipContent>{tooltip}</TooltipContent>
|
||||||
|
</TooltipRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ export const StyledTooltipContent = styled(
|
||||||
color: var(--color-fg-on-assistive);
|
color: var(--color-fg-on-assistive);
|
||||||
padding: var(--spacing-2) var(--spacing-3);
|
padding: var(--spacing-2) var(--spacing-3);
|
||||||
border-radius: var(--border-radius-1);
|
border-radius: var(--border-radius-1);
|
||||||
|
z-index: var(--z-index-99);
|
||||||
|
|
||||||
[data-tooltip-trigger-arrow] {
|
[data-tooltip-trigger-arrow] {
|
||||||
fill: var(--color-bg-assistive);
|
fill: var(--color-bg-assistive);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export { Tooltip } from "@design-system/headless";
|
export { Tooltip } from "./Tooltip";
|
||||||
|
export { TooltipRoot } from "@design-system/headless";
|
||||||
export { TooltipTrigger } from "./TooltipTrigger";
|
export { TooltipTrigger } from "./TooltipTrigger";
|
||||||
export { TooltipContent } from "./TooltipContent";
|
export { TooltipContent } from "./TooltipContent";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
// components
|
// components
|
||||||
|
export { Icon } from "@design-system/headless";
|
||||||
|
|
||||||
export * from "./components/Button";
|
export * from "./components/Button";
|
||||||
export * from "./components/Checkbox";
|
export * from "./components/Checkbox";
|
||||||
export * from "./components/Text";
|
export * from "./components/Text";
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
Text,
|
Text,
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Tooltip,
|
TooltipRoot,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
} from "../";
|
} from "../";
|
||||||
|
|
@ -34,14 +34,14 @@ export const ComplexForm = () => {
|
||||||
gap: "var(--spacing-2)",
|
gap: "var(--spacing-2)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<TooltipRoot>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button variant="outlined">Cancel</Button>
|
<Button variant="outlined">Cancel</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
If you cancel, you will lose your order
|
If you cancel, you will lose your order
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipRoot>
|
||||||
<Button>Ok</Button>
|
<Button>Ok</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,5 @@ export type OmitRename<
|
||||||
TOmitKeys extends keyof TObj,
|
TOmitKeys extends keyof TObj,
|
||||||
TSymbol extends string = "$",
|
TSymbol extends string = "$",
|
||||||
> = {
|
> = {
|
||||||
[K in keyof TObj as K extends TOmitKeys
|
[K in keyof Omit<TObj, TOmitKeys> as `${TSymbol}${string & K}`]: TObj[K];
|
||||||
? never
|
} & Pick<TObj, TOmitKeys>;
|
||||||
: `${TSymbol}${string & K}`]: TObj[K];
|
|
||||||
} & Omit<TObj, TOmitKeys>;
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
TokensAccessor,
|
TokensAccessor,
|
||||||
defaultTokens,
|
defaultTokens,
|
||||||
useFluidTokens,
|
useFluidTokens,
|
||||||
|
useTheme,
|
||||||
} from "@design-system/theming";
|
} from "@design-system/theming";
|
||||||
import Color from "colorjs.io";
|
import Color from "colorjs.io";
|
||||||
|
|
||||||
|
|
@ -22,132 +23,14 @@ const StyledThemeProvider = styled(ThemeProvider)`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const theming = (Story, args) => {
|
export const theming = (Story, args) => {
|
||||||
const [rootUnitRatio, setRootUnitRatio] = useState(1);
|
const { theme } = useTheme({
|
||||||
const { fluid, ...restDefaultTokens } = defaultTokens;
|
seedColor: args.globals.accentColor,
|
||||||
|
colorMode: args.globals.colorMode,
|
||||||
const { typography, rootUnit, spacing, sizing } = useFluidTokens(
|
borderRadius: args.globals.borderRadius,
|
||||||
fluid,
|
fontFamily: args.globals.fontFamily,
|
||||||
rootUnitRatio,
|
rootUnitRatio: args.globals.rootUnitRatio,
|
||||||
);
|
|
||||||
|
|
||||||
const tokensAccessor = new TokensAccessor({
|
|
||||||
...restDefaultTokens,
|
|
||||||
rootUnit,
|
|
||||||
spacing,
|
|
||||||
sizing,
|
|
||||||
typography,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [theme, setTheme] = useState(tokensAccessor.getAllTokens());
|
|
||||||
|
|
||||||
const updateFontFamily = (fontFamily) => {
|
|
||||||
tokensAccessor.updateFontFamily(fontFamily);
|
|
||||||
|
|
||||||
setTheme((prevState) => {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
typography: tokensAccessor.getTypography(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (args.globals.colorMode) {
|
|
||||||
tokensAccessor.updateColorMode(args.globals.colorMode);
|
|
||||||
|
|
||||||
setTheme((prevState) => {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
...tokensAccessor.getColors(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [args.globals.colorMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (args.globals.borderRadius) {
|
|
||||||
tokensAccessor.updateBorderRadius({
|
|
||||||
1: args.globals.borderRadius,
|
|
||||||
});
|
|
||||||
|
|
||||||
setTheme((prevState) => {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
...tokensAccessor.getBorderRadius(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [args.globals.borderRadius]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (args.globals.accentColor) {
|
|
||||||
let color;
|
|
||||||
|
|
||||||
try {
|
|
||||||
color = Color.parse(args.globals.accentColor);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (color) {
|
|
||||||
tokensAccessor.updateSeedColor(args.globals.accentColor);
|
|
||||||
|
|
||||||
setTheme((prevState) => {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
...tokensAccessor.getColors(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [args.globals.accentColor]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
args.globals.fontFamily &&
|
|
||||||
args.globals.fontFamily !== "Arial" &&
|
|
||||||
args.globals.fontFamily !== "System Default"
|
|
||||||
) {
|
|
||||||
webfontloader.load({
|
|
||||||
google: {
|
|
||||||
families: [`${args.globals.fontFamily}:300,400,500,700`],
|
|
||||||
},
|
|
||||||
active: () => {
|
|
||||||
updateFontFamily(args.globals.fontFamily);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
updateFontFamily(args.globals.fontFamily);
|
|
||||||
}
|
|
||||||
}, [args.globals.fontFamily]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (args.globals.rootUnitRatio) {
|
|
||||||
setRootUnitRatio(args.globals.rootUnitRatio);
|
|
||||||
tokensAccessor.updateRootUnit(rootUnit);
|
|
||||||
tokensAccessor.updateSpacing(spacing);
|
|
||||||
|
|
||||||
setTheme((prevState) => {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
rootUnit: tokensAccessor.getRootUnit(),
|
|
||||||
...tokensAccessor.getSpacing(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [args.globals.rootUnitRatio]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
tokensAccessor.updateTypography(typography);
|
|
||||||
|
|
||||||
setTheme((prevState) => {
|
|
||||||
return {
|
|
||||||
...prevState,
|
|
||||||
typography: tokensAccessor.getTypography(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [typography]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledThemeProvider theme={theme}>
|
<StyledThemeProvider theme={theme}>
|
||||||
<Story />
|
<Story />
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,14 @@ body,
|
||||||
.innerZoomElementWrapper > * {
|
.innerZoomElementWrapper > * {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* fonts */
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export const FEATURE_FLAG = {
|
||||||
ab_ds_schema_enabled: "ab_ds_schema_enabled",
|
ab_ds_schema_enabled: "ab_ds_schema_enabled",
|
||||||
ab_ds_binding_enabled: "ab_ds_binding_enabled",
|
ab_ds_binding_enabled: "ab_ds_binding_enabled",
|
||||||
release_scim_provisioning_enabled: "release_scim_provisioning_enabled",
|
release_scim_provisioning_enabled: "release_scim_provisioning_enabled",
|
||||||
|
ab_wds_enabled: "ab_wds_enabled",
|
||||||
release_widgetdiscovery_enabled: "release_widgetdiscovery_enabled",
|
release_widgetdiscovery_enabled: "release_widgetdiscovery_enabled",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
|
||||||
ab_ds_schema_enabled: false,
|
ab_ds_schema_enabled: false,
|
||||||
ab_ds_binding_enabled: false,
|
ab_ds_binding_enabled: false,
|
||||||
release_scim_provisioning_enabled: false,
|
release_scim_provisioning_enabled: false,
|
||||||
|
ab_wds_enabled: false,
|
||||||
release_widgetdiscovery_enabled: false,
|
release_widgetdiscovery_enabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { createSelector } from "reselect";
|
||||||
import type { AppState } from "@appsmith/reducers";
|
import type { AppState } from "@appsmith/reducers";
|
||||||
import type { FeatureFlag } from "@appsmith/entities/FeatureFlag";
|
import type { FeatureFlag } from "@appsmith/entities/FeatureFlag";
|
||||||
import { createSelector } from "reselect";
|
|
||||||
|
|
||||||
export const selectFeatureFlags = (state: AppState) =>
|
export const selectFeatureFlags = (state: AppState) =>
|
||||||
state.ui.users.featureFlag.data;
|
state.ui.users.featureFlag.data;
|
||||||
|
|
|
||||||
3
app/client/src/components/wds/constants.ts
Normal file
3
app/client/src/components/wds/constants.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const WDS_V2_WIDGET_MAP = {
|
||||||
|
BUTTON_WIDGET: "BUTTON_WIDGET_V2",
|
||||||
|
};
|
||||||
|
|
@ -37,13 +37,18 @@ import { WidgetGlobaStyles } from "globalStyles/WidgetGlobalStyles";
|
||||||
import { getAppsmithConfigs } from "@appsmith/configs";
|
import { getAppsmithConfigs } from "@appsmith/configs";
|
||||||
import useWidgetFocus from "utils/hooks/useWidgetFocus/useWidgetFocus";
|
import useWidgetFocus from "utils/hooks/useWidgetFocus/useWidgetFocus";
|
||||||
import HtmlTitle from "./AppViewerHtmlTitle";
|
import HtmlTitle from "./AppViewerHtmlTitle";
|
||||||
|
import BottomBar from "components/BottomBar";
|
||||||
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
|
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
|
||||||
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
|
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
|
||||||
import { editorInitializer } from "../../utils/editor/EditorUtils";
|
import { editorInitializer } from "../../utils/editor/EditorUtils";
|
||||||
import { widgetInitialisationSuccess } from "../../actions/widgetActions";
|
import { widgetInitialisationSuccess } from "../../actions/widgetActions";
|
||||||
import BottomBar from "components/BottomBar";
|
|
||||||
import { areEnvironmentsFetched } from "@appsmith/selectors/environmentSelectors";
|
import { areEnvironmentsFetched } from "@appsmith/selectors/environmentSelectors";
|
||||||
import { datasourceEnvEnabled } from "@appsmith/selectors/featureFlagsSelectors";
|
import { datasourceEnvEnabled } from "@appsmith/selectors/featureFlagsSelectors";
|
||||||
|
import {
|
||||||
|
ThemeProvider as WDSThemeProvider,
|
||||||
|
useTheme,
|
||||||
|
} from "@design-system/theming";
|
||||||
|
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||||
|
|
||||||
const AppViewerBody = styled.section<{
|
const AppViewerBody = styled.section<{
|
||||||
hasPages: boolean;
|
hasPages: boolean;
|
||||||
|
|
@ -98,7 +103,10 @@ function AppViewer(props: Props) {
|
||||||
const currentApplicationDetails: ApplicationPayload | undefined = useSelector(
|
const currentApplicationDetails: ApplicationPayload | undefined = useSelector(
|
||||||
getCurrentApplication,
|
getCurrentApplication,
|
||||||
);
|
);
|
||||||
|
const { theme } = useTheme({
|
||||||
|
borderRadius: selectedTheme.properties.borderRadius.appBorderRadius,
|
||||||
|
seedColor: selectedTheme.properties.colors.primaryColor,
|
||||||
|
});
|
||||||
const focusRef = useWidgetFocus();
|
const focusRef = useWidgetFocus();
|
||||||
|
|
||||||
const workspaceId = currentApplicationDetails?.workspaceId || "";
|
const workspaceId = currentApplicationDetails?.workspaceId || "";
|
||||||
|
|
@ -176,20 +184,26 @@ function AppViewer(props: Props) {
|
||||||
};
|
};
|
||||||
}, [selectedTheme.properties.fontFamily.appFont]);
|
}, [selectedTheme.properties.fontFamily.appFont]);
|
||||||
|
|
||||||
|
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
|
||||||
|
const backgroundForBody = isWDSV2Enabled
|
||||||
|
? "var(--color-bg)"
|
||||||
|
: selectedTheme.properties.colors.backgroundColor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<WDSThemeProvider theme={theme}>
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ThemeProvider theme={lightTheme}>
|
||||||
<EditorContextProvider renderMode="PAGE">
|
<EditorContextProvider renderMode="PAGE">
|
||||||
|
{!isWDSV2Enabled && (
|
||||||
<WidgetGlobaStyles
|
<WidgetGlobaStyles
|
||||||
fontFamily={selectedTheme.properties.fontFamily.appFont}
|
fontFamily={selectedTheme.properties.fontFamily.appFont}
|
||||||
primaryColor={selectedTheme.properties.colors.primaryColor}
|
primaryColor={selectedTheme.properties.colors.primaryColor}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<HtmlTitle
|
<HtmlTitle
|
||||||
description={pageDescription}
|
description={pageDescription}
|
||||||
name={currentApplicationDetails?.name}
|
name={currentApplicationDetails?.name}
|
||||||
/>
|
/>
|
||||||
<AppViewerBodyContainer
|
<AppViewerBodyContainer backgroundColor={backgroundForBody}>
|
||||||
backgroundColor={selectedTheme.properties.colors.backgroundColor}
|
|
||||||
>
|
|
||||||
<AppViewerBody
|
<AppViewerBody
|
||||||
className={CANVAS_SELECTOR}
|
className={CANVAS_SELECTOR}
|
||||||
hasPages={pages.length > 1}
|
hasPages={pages.length > 1}
|
||||||
|
|
@ -216,6 +230,7 @@ function AppViewer(props: Props) {
|
||||||
</AppViewerBodyContainer>
|
</AppViewerBodyContainer>
|
||||||
</EditorContextProvider>
|
</EditorContextProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</WDSThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
import WidgetFactory from "utils/WidgetFactory";
|
import WidgetFactory from "utils/WidgetFactory";
|
||||||
import type { CanvasWidgetStructure } from "widgets/constants";
|
import type { CanvasWidgetStructure } from "widgets/constants";
|
||||||
|
|
||||||
import { RenderModes } from "constants/WidgetConstants";
|
import { RenderModes } from "constants/WidgetConstants";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
|
|
||||||
import { previewModeSelector } from "selectors/editorSelectors";
|
|
||||||
import useWidgetFocus from "utils/hooks/useWidgetFocus";
|
import useWidgetFocus from "utils/hooks/useWidgetFocus";
|
||||||
|
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||||
|
import { previewModeSelector } from "selectors/editorSelectors";
|
||||||
|
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
|
||||||
import { getViewportClassName } from "utils/autoLayout/AutoLayoutUtils";
|
import { getViewportClassName } from "utils/autoLayout/AutoLayoutUtils";
|
||||||
|
import {
|
||||||
|
ThemeProvider as WDSThemeProvider,
|
||||||
|
useTheme,
|
||||||
|
} from "@design-system/theming";
|
||||||
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
|
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
|
||||||
|
|
||||||
interface CanvasProps {
|
interface CanvasProps {
|
||||||
|
|
@ -36,6 +41,11 @@ const Canvas = (props: CanvasProps) => {
|
||||||
getIsAppSettingsPaneWithNavigationTabOpen,
|
getIsAppSettingsPaneWithNavigationTabOpen,
|
||||||
);
|
);
|
||||||
const selectedTheme = useSelector(getSelectedAppTheme);
|
const selectedTheme = useSelector(getSelectedAppTheme);
|
||||||
|
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
|
||||||
|
const { theme } = useTheme({
|
||||||
|
borderRadius: selectedTheme.properties.borderRadius.appBorderRadius,
|
||||||
|
seedColor: selectedTheme.properties.colors.primaryColor,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* background for canvas
|
* background for canvas
|
||||||
|
|
@ -43,10 +53,18 @@ const Canvas = (props: CanvasProps) => {
|
||||||
let backgroundForCanvas;
|
let backgroundForCanvas;
|
||||||
|
|
||||||
if (isPreviewMode || isAppSettingsPaneWithNavigationTabOpen) {
|
if (isPreviewMode || isAppSettingsPaneWithNavigationTabOpen) {
|
||||||
|
if (isWDSV2Enabled) {
|
||||||
|
backgroundForCanvas = "var(--color-bg)";
|
||||||
|
} else {
|
||||||
backgroundForCanvas = "initial";
|
backgroundForCanvas = "initial";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isWDSV2Enabled) {
|
||||||
|
backgroundForCanvas = "var(--color-bg)";
|
||||||
} else {
|
} else {
|
||||||
backgroundForCanvas = selectedTheme.properties.colors.backgroundColor;
|
backgroundForCanvas = selectedTheme.properties.colors.backgroundColor;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const focusRef = useWidgetFocus();
|
const focusRef = useWidgetFocus();
|
||||||
|
|
||||||
|
|
@ -54,6 +72,7 @@ const Canvas = (props: CanvasProps) => {
|
||||||
const paddingBottomClass = props.isAutoLayout ? "" : "pb-52";
|
const paddingBottomClass = props.isAutoLayout ? "" : "pb-52";
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
|
<WDSThemeProvider theme={theme}>
|
||||||
<Container
|
<Container
|
||||||
$isAutoLayout={!!props.isAutoLayout}
|
$isAutoLayout={!!props.isAutoLayout}
|
||||||
background={backgroundForCanvas}
|
background={backgroundForCanvas}
|
||||||
|
|
@ -71,6 +90,7 @@ const Canvas = (props: CanvasProps) => {
|
||||||
RenderModes.CANVAS,
|
RenderModes.CANVAS,
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
</WDSThemeProvider>
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Error rendering DSL", error);
|
log.error("Error rendering DSL", error);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export const excludeList: WidgetType[] = [
|
||||||
"FILE_PICKER_WIDGET",
|
"FILE_PICKER_WIDGET",
|
||||||
"FILE_PICKER_WIDGET_V2",
|
"FILE_PICKER_WIDGET_V2",
|
||||||
"TABLE_WIDGET_V2",
|
"TABLE_WIDGET_V2",
|
||||||
|
"BUTTON_WIDGET_V2",
|
||||||
];
|
];
|
||||||
|
|
||||||
function PropertyPaneView(
|
function PropertyPaneView(
|
||||||
|
|
@ -184,7 +185,7 @@ function PropertyPaneView(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full overflow-y-scroll h-full"
|
className="w-full h-full overflow-y-scroll"
|
||||||
key={`property-pane-${widgetProperties.widgetId}`}
|
key={`property-pane-${widgetProperties.widgetId}`}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,16 @@ import {
|
||||||
getSelectedAppTheme,
|
getSelectedAppTheme,
|
||||||
} from "selectors/appThemingSelectors";
|
} from "selectors/appThemingSelectors";
|
||||||
import { getIsAutoLayout } from "selectors/canvasSelectors";
|
import { getIsAutoLayout } from "selectors/canvasSelectors";
|
||||||
import { getCanvasWidgetsStructure } from "selectors/entitiesSelector";
|
|
||||||
import { getCurrentThemeDetails } from "selectors/themeSelectors";
|
import { getCurrentThemeDetails } from "selectors/themeSelectors";
|
||||||
|
import { getCanvasWidgetsStructure } from "selectors/entitiesSelector";
|
||||||
import {
|
import {
|
||||||
AUTOLAYOUT_RESIZER_WIDTH_BUFFER,
|
AUTOLAYOUT_RESIZER_WIDTH_BUFFER,
|
||||||
useDynamicAppLayout,
|
useDynamicAppLayout,
|
||||||
} from "utils/hooks/useDynamicAppLayout";
|
} from "utils/hooks/useDynamicAppLayout";
|
||||||
import Canvas from "../Canvas";
|
import Canvas from "../Canvas";
|
||||||
import { CanvasResizer } from "widgets/CanvasResizer";
|
|
||||||
import type { AppState } from "@appsmith/reducers";
|
import type { AppState } from "@appsmith/reducers";
|
||||||
|
import { CanvasResizer } from "widgets/CanvasResizer";
|
||||||
|
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||||
import { getIsAnonymousDataPopupVisible } from "selectors/onboardingSelectors";
|
import { getIsAnonymousDataPopupVisible } from "selectors/onboardingSelectors";
|
||||||
|
|
||||||
type CanvasContainerProps = {
|
type CanvasContainerProps = {
|
||||||
|
|
@ -119,9 +120,9 @@ function CanvasContainer(props: CanvasContainerProps) {
|
||||||
const isAppThemeChanging = useSelector(getAppThemeIsChanging);
|
const isAppThemeChanging = useSelector(getAppThemeIsChanging);
|
||||||
const showCanvasTopSection = useSelector(showCanvasTopSectionSelector);
|
const showCanvasTopSection = useSelector(showCanvasTopSectionSelector);
|
||||||
const showAnonymousDataPopup = useSelector(getIsAnonymousDataPopupVisible);
|
const showAnonymousDataPopup = useSelector(getIsAnonymousDataPopupVisible);
|
||||||
|
|
||||||
const isLayoutingInitialized = useDynamicAppLayout();
|
const isLayoutingInitialized = useDynamicAppLayout();
|
||||||
const isPageInitializing = isFetchingPage || !isLayoutingInitialized;
|
const isPageInitializing = isFetchingPage || !isLayoutingInitialized;
|
||||||
|
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -184,7 +185,9 @@ function CanvasContainer(props: CanvasContainerProps) {
|
||||||
$isAutoLayout={isAutoLayout}
|
$isAutoLayout={isAutoLayout}
|
||||||
background={
|
background={
|
||||||
isPreviewMode || isAppSettingsPaneWithNavigationTabOpen
|
isPreviewMode || isAppSettingsPaneWithNavigationTabOpen
|
||||||
? selectedTheme.properties.colors.backgroundColor
|
? isWDSV2Enabled
|
||||||
|
? "var(--bg-color)"
|
||||||
|
: selectedTheme.properties.colors.backgroundColor
|
||||||
: "initial"
|
: "initial"
|
||||||
}
|
}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
|
@ -214,10 +217,12 @@ function CanvasContainer(props: CanvasContainerProps) {
|
||||||
pointerEvents: isAutoCanvasResizing ? "none" : "auto",
|
pointerEvents: isAutoCanvasResizing ? "none" : "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!isWDSV2Enabled && (
|
||||||
<WidgetGlobaStyles
|
<WidgetGlobaStyles
|
||||||
fontFamily={selectedTheme.properties.fontFamily.appFont}
|
fontFamily={selectedTheme.properties.fontFamily.appFont}
|
||||||
primaryColor={selectedTheme.properties.colors.primaryColor}
|
primaryColor={selectedTheme.properties.colors.primaryColor}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{isAppThemeChanging && (
|
{isAppThemeChanging && (
|
||||||
<div className="fixed top-0 bottom-0 left-0 right-0 flex items-center justify-center bg-white/70 z-[2]">
|
<div className="fixed top-0 bottom-0 left-0 right-0 flex items-center justify-center bg-white/70 z-[2]">
|
||||||
<Spinner size="md" />
|
<Spinner size="md" />
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const initialState: AppThemingState = {
|
||||||
properties: {
|
properties: {
|
||||||
colors: {
|
colors: {
|
||||||
backgroundColor: "#F8FAFC",
|
backgroundColor: "#F8FAFC",
|
||||||
primaryColor: "",
|
primaryColor: "#000",
|
||||||
secondaryColor: "",
|
secondaryColor: "",
|
||||||
},
|
},
|
||||||
borderRadius: {},
|
borderRadius: {},
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import type {
|
||||||
AppLayoutConfig,
|
AppLayoutConfig,
|
||||||
PageListReduxState,
|
PageListReduxState,
|
||||||
} from "reducers/entityReducers/pageListReducer";
|
} from "reducers/entityReducers/pageListReducer";
|
||||||
import type { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer";
|
|
||||||
import type { WidgetCardProps, WidgetProps } from "widgets/BaseWidget";
|
import type { WidgetCardProps, WidgetProps } from "widgets/BaseWidget";
|
||||||
|
|
||||||
import type { Page } from "@appsmith/constants/ReduxActionConstants";
|
import type { Page } from "@appsmith/constants/ReduxActionConstants";
|
||||||
|
|
@ -61,6 +60,9 @@ import WidgetFactory from "utils/WidgetFactory";
|
||||||
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
|
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
|
||||||
import { nestDSL } from "@shared/dsl";
|
import { nestDSL } from "@shared/dsl";
|
||||||
import { getIsAnonymousDataPopupVisible } from "./onboardingSelectors";
|
import { getIsAnonymousDataPopupVisible } from "./onboardingSelectors";
|
||||||
|
import { WDS_V2_WIDGET_MAP } from "components/wds/constants";
|
||||||
|
import { selectFeatureFlagCheck } from "@appsmith/selectors/featureFlagsSelectors";
|
||||||
|
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
|
||||||
|
|
||||||
const getIsDraggingOrResizing = (state: AppState) =>
|
const getIsDraggingOrResizing = (state: AppState) =>
|
||||||
state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging;
|
state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging;
|
||||||
|
|
@ -336,12 +338,29 @@ export const getCurrentPageName = createSelector(
|
||||||
export const getWidgetCards = createSelector(
|
export const getWidgetCards = createSelector(
|
||||||
getWidgetConfigs,
|
getWidgetConfigs,
|
||||||
getIsAutoLayout,
|
getIsAutoLayout,
|
||||||
(widgetConfigs: WidgetConfigReducerState, isAutoLayout: boolean) => {
|
(_state) => selectFeatureFlagCheck(_state, FEATURE_FLAG.ab_wds_enabled),
|
||||||
|
(widgetConfigs, isAutoLayout, isWDSEnabled) => {
|
||||||
const cards = Object.values(widgetConfigs.config).filter((config) => {
|
const cards = Object.values(widgetConfigs.config).filter((config) => {
|
||||||
return isAirgapped()
|
if (isAirgapped()) {
|
||||||
? config.widgetName !== "Map" && !config.hideCard
|
return config.widgetName !== "Map" && !config.hideCard;
|
||||||
: !config.hideCard;
|
}
|
||||||
|
|
||||||
|
// if wds_vs is not enabled, hide all wds_v2 widgets
|
||||||
|
if (
|
||||||
|
Object.values(WDS_V2_WIDGET_MAP).includes(config.type) &&
|
||||||
|
isWDSEnabled === false
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if wds is enabled, only show the wds_v2 widgets
|
||||||
|
if (isWDSEnabled === true) {
|
||||||
|
return Object.values(WDS_V2_WIDGET_MAP).includes(config.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !config.hideCard;
|
||||||
});
|
});
|
||||||
|
|
||||||
const _cards: WidgetCardProps[] = cards.map((config) => {
|
const _cards: WidgetCardProps[] = cards.map((config) => {
|
||||||
const {
|
const {
|
||||||
detachFromLayout = false,
|
detachFromLayout = false,
|
||||||
|
|
@ -374,6 +393,7 @@ export const getWidgetCards = createSelector(
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const sortedCards = sortBy(_cards, ["displayName"]);
|
const sortedCards = sortBy(_cards, ["displayName"]);
|
||||||
|
|
||||||
return sortedCards;
|
return sortedCards;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,10 @@ import CodeScannerWidget, {
|
||||||
import ListWidgetV2, {
|
import ListWidgetV2, {
|
||||||
CONFIG as LIST_WIDGET_CONFIG_V2,
|
CONFIG as LIST_WIDGET_CONFIG_V2,
|
||||||
} from "widgets/ListWidgetV2";
|
} from "widgets/ListWidgetV2";
|
||||||
|
import {
|
||||||
|
ButtonWidget as ButtonWidgetV2,
|
||||||
|
CONFIG as BUTTON_WIDGET_CONFIG_V2,
|
||||||
|
} from "widgets/ButtonWidgetV2";
|
||||||
|
|
||||||
export const ALL_WIDGETS_AND_CONFIG: [any, WidgetConfiguration][] = [
|
export const ALL_WIDGETS_AND_CONFIG: [any, WidgetConfiguration][] = [
|
||||||
[CanvasWidget, CANVAS_WIDGET_CONFIG],
|
[CanvasWidget, CANVAS_WIDGET_CONFIG],
|
||||||
|
|
@ -215,6 +219,7 @@ export const ALL_WIDGETS_AND_CONFIG: [any, WidgetConfiguration][] = [
|
||||||
[CategorySliderWidget, CATEGORY_SLIDER_WIDGET_CONFIG],
|
[CategorySliderWidget, CATEGORY_SLIDER_WIDGET_CONFIG],
|
||||||
[CodeScannerWidget, CODE_SCANNER_WIDGET_CONFIG],
|
[CodeScannerWidget, CODE_SCANNER_WIDGET_CONFIG],
|
||||||
[ListWidgetV2, LIST_WIDGET_CONFIG_V2],
|
[ListWidgetV2, LIST_WIDGET_CONFIG_V2],
|
||||||
|
[ButtonWidgetV2, BUTTON_WIDGET_CONFIG_V2],
|
||||||
|
|
||||||
//Deprecated Widgets
|
//Deprecated Widgets
|
||||||
[InputWidget, INPUT_WIDGET_CONFIG],
|
[InputWidget, INPUT_WIDGET_CONFIG],
|
||||||
|
|
|
||||||
11
app/client/src/utils/hooks/useFeatureFlag.ts
Normal file
11
app/client/src/utils/hooks/useFeatureFlag.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import type { FeatureFlag } from "@appsmith/entities/FeatureFlag";
|
||||||
|
import { selectFeatureFlags } from "@appsmith/selectors/featureFlagsSelectors";
|
||||||
|
|
||||||
|
export function useFeatureFlag(flagName: FeatureFlag): boolean {
|
||||||
|
const flagValues = useSelector(selectFeatureFlags);
|
||||||
|
if (flagName in flagValues) {
|
||||||
|
return flagValues[flagName];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from "react";
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
|
import type { RenderMode } from "constants/WidgetConstants";
|
||||||
|
|
||||||
|
const StyledContainer = styled.div<ContainerProps>`
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
${({ maxWidth, minHeight, minWidth }) =>
|
||||||
|
css`
|
||||||
|
& [data-button] {
|
||||||
|
display: flex;
|
||||||
|
width: auto;
|
||||||
|
${minWidth ? `min-width: ${minWidth}px;` : ""}
|
||||||
|
${minHeight ? `min-height: ${minHeight}px;` : ""}
|
||||||
|
${maxWidth ? `max-width: ${maxWidth}px;` : ""}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
|
||||||
|
.grecaptcha-badge {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ContainerProps = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
renderMode?: RenderMode;
|
||||||
|
showInAllModes?: boolean;
|
||||||
|
minWidth?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Container(props: ContainerProps) {
|
||||||
|
const { children, ...rest } = props;
|
||||||
|
|
||||||
|
return <StyledContainer {...rest}>{children}</StyledContainer>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import React from "react";
|
||||||
|
import { noop } from "lodash";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import ReCAPTCHA from "react-google-recaptcha";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GOOGLE_RECAPTCHA_KEY_ERROR,
|
||||||
|
GOOGLE_RECAPTCHA_DOMAIN_ERROR,
|
||||||
|
createMessage,
|
||||||
|
} from "@appsmith/constants/messages";
|
||||||
|
import type { RecaptchaProps } from "./useRecaptcha";
|
||||||
|
|
||||||
|
export type RecaptchaV2Props = RecaptchaProps;
|
||||||
|
|
||||||
|
export function RecaptchaV2(props: RecaptchaV2Props) {
|
||||||
|
const recaptchaRef = useRef<ReCAPTCHA>(null);
|
||||||
|
const [isInvalidKey, setInvalidKey] = useState(false);
|
||||||
|
const handleRecaptchaLoading = (isloading: boolean) => {
|
||||||
|
props.handleRecaptchaV2Loading && props.handleRecaptchaV2Loading(isloading);
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isDisabled,
|
||||||
|
recaptchaKey,
|
||||||
|
onRecaptchaSubmitSuccess,
|
||||||
|
onRecaptchaSubmitError = noop,
|
||||||
|
onPress: onClickProp,
|
||||||
|
} = props;
|
||||||
|
const onClick = () => {
|
||||||
|
if (isDisabled) return onClickProp;
|
||||||
|
if (isLoading) return onClickProp;
|
||||||
|
|
||||||
|
if (isInvalidKey) {
|
||||||
|
// Handle incorrent google recaptcha site key
|
||||||
|
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
|
||||||
|
} else {
|
||||||
|
handleRecaptchaLoading(true);
|
||||||
|
recaptchaRef?.current?.reset();
|
||||||
|
recaptchaRef?.current
|
||||||
|
?.executeAsync()
|
||||||
|
.then((token: any) => {
|
||||||
|
if (token) {
|
||||||
|
if (typeof onRecaptchaSubmitSuccess === "function") {
|
||||||
|
onRecaptchaSubmitSuccess(token);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle incorrent google recaptcha site key
|
||||||
|
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRecaptchaLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
handleRecaptchaLoading(false);
|
||||||
|
// Handle error due to google recaptcha key of different domain
|
||||||
|
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recaptcha = (
|
||||||
|
<ReCAPTCHA
|
||||||
|
onErrored={() => setInvalidKey(true)}
|
||||||
|
ref={recaptchaRef}
|
||||||
|
sitekey={recaptchaKey || ""}
|
||||||
|
size="invisible"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return { onClick, recaptcha };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { noop } from "lodash";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GOOGLE_RECAPTCHA_KEY_ERROR,
|
||||||
|
GOOGLE_RECAPTCHA_DOMAIN_ERROR,
|
||||||
|
createMessage,
|
||||||
|
} from "@appsmith/constants/messages";
|
||||||
|
import type { ButtonComponentProps } from ".";
|
||||||
|
import type { RecaptchaProps } from "./useRecaptcha";
|
||||||
|
import { useScript, ScriptStatus, AddScriptTo } from "utils/hooks/useScript";
|
||||||
|
|
||||||
|
type RecaptchaV3Props = RecaptchaProps;
|
||||||
|
|
||||||
|
export function RecaptchaV3(props: RecaptchaV3Props) {
|
||||||
|
const checkValidJson = (inputString: string): boolean => {
|
||||||
|
return !inputString.includes('"');
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
recaptchaKey,
|
||||||
|
onPress: onClickProp,
|
||||||
|
onRecaptchaSubmitSuccess,
|
||||||
|
onRecaptchaSubmitError = noop,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const onClick: ButtonComponentProps["onPress"] = () => {
|
||||||
|
if (props.isDisabled) return onClickProp;
|
||||||
|
if (props.isLoading) return onClickProp;
|
||||||
|
|
||||||
|
if (status === ScriptStatus.READY) {
|
||||||
|
(window as any).grecaptcha.ready(() => {
|
||||||
|
try {
|
||||||
|
(window as any).grecaptcha
|
||||||
|
.execute(recaptchaKey, {
|
||||||
|
action: "submit",
|
||||||
|
})
|
||||||
|
.then((token: any) => {
|
||||||
|
if (typeof onRecaptchaSubmitSuccess === "function") {
|
||||||
|
onRecaptchaSubmitSuccess(token);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Handle incorrent google recaptcha site key
|
||||||
|
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_KEY_ERROR));
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Handle error due to google recaptcha key of different domain
|
||||||
|
onRecaptchaSubmitError(createMessage(GOOGLE_RECAPTCHA_DOMAIN_ERROR));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let validGoogleRecaptchaKey = recaptchaKey;
|
||||||
|
if (validGoogleRecaptchaKey && !checkValidJson(validGoogleRecaptchaKey)) {
|
||||||
|
validGoogleRecaptchaKey = undefined;
|
||||||
|
}
|
||||||
|
const status = useScript(
|
||||||
|
`https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`,
|
||||||
|
AddScriptTo.HEAD,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { onClick };
|
||||||
|
}
|
||||||
53
app/client/src/widgets/ButtonWidgetV2/component/index.tsx
Normal file
53
app/client/src/widgets/ButtonWidgetV2/component/index.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Icon as BIcon } from "@blueprintjs/core";
|
||||||
|
import type { IconName } from "@blueprintjs/icons";
|
||||||
|
|
||||||
|
import { Container } from "./Container";
|
||||||
|
import { useRecaptcha } from "./useRecaptcha";
|
||||||
|
import type { UseRecaptchaProps } from "./useRecaptcha";
|
||||||
|
import type { ButtonProps } from "@design-system/widgets";
|
||||||
|
import { Button, Icon, Tooltip } from "@design-system/widgets";
|
||||||
|
|
||||||
|
export type ButtonComponentProps = {
|
||||||
|
text?: string;
|
||||||
|
tooltip?: string;
|
||||||
|
minWidth?: number;
|
||||||
|
maxWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
isVisible?: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
iconName?: IconName;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
variant?: ButtonProps["variant"];
|
||||||
|
color?: ButtonProps["color"];
|
||||||
|
type: ButtonProps["type"];
|
||||||
|
onPress?: ButtonProps["onPress"];
|
||||||
|
iconPosition?: ButtonProps["iconPosition"];
|
||||||
|
};
|
||||||
|
|
||||||
|
function ButtonComponent(props: ButtonComponentProps & UseRecaptchaProps) {
|
||||||
|
const { iconName, maxWidth, minHeight, minWidth, text, tooltip, ...rest } =
|
||||||
|
props;
|
||||||
|
const containerProps = { maxWidth, minHeight, minWidth };
|
||||||
|
|
||||||
|
const icon = iconName && (
|
||||||
|
<Icon>
|
||||||
|
<BIcon icon={iconName} />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { onClick, recpatcha } = useRecaptcha(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container {...containerProps}>
|
||||||
|
<Tooltip tooltip={tooltip}>
|
||||||
|
<Button icon={icon} {...rest} onPress={onClick}>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{recpatcha}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ButtonComponent;
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { RecaptchaV2 } from "./RecaptchaV2";
|
||||||
|
import { RecaptchaV3 } from "./RecaptchaV3";
|
||||||
|
import type { ButtonComponentProps } from ".";
|
||||||
|
import type { RecaptchaType } from "components/constants";
|
||||||
|
|
||||||
|
export type UseRecaptchaProps = {
|
||||||
|
recaptchaKey?: string;
|
||||||
|
recaptchaType?: RecaptchaType;
|
||||||
|
onRecaptchaSubmitError?: (error: string) => void;
|
||||||
|
onRecaptchaSubmitSuccess?: (token: string) => void;
|
||||||
|
handleRecaptchaV2Loading?: (isLoading: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecaptchaProps = ButtonComponentProps & UseRecaptchaProps;
|
||||||
|
|
||||||
|
type UseRecaptchaReturn = {
|
||||||
|
onClick?: (...args: any[]) => void;
|
||||||
|
recpatcha?: React.ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRecaptcha = (props: RecaptchaProps): UseRecaptchaReturn => {
|
||||||
|
const { onPress: onClickProp, recaptchaKey } = props;
|
||||||
|
|
||||||
|
if (!recaptchaKey) {
|
||||||
|
return { onClick: onClickProp };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.recaptchaType === "V2") {
|
||||||
|
return RecaptchaV2(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.recaptchaType === "V3") {
|
||||||
|
return RecaptchaV3(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { onClick: onClickProp };
|
||||||
|
};
|
||||||
1
app/client/src/widgets/ButtonWidgetV2/icon.svg
Normal file
1
app/client/src/widgets/ButtonWidgetV2/icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m24 19v-8h-16v10h8v-2h-6v-6h12v6zm-9.4727 3.0555c1.1911-.4051 1.3299-.3724 2.2775.6936.0151.0138.0302.0277.0453.0416.239.2192.4717.4326.7838.3138.3327-.1256.4008-.3158.4008-.6439v-6.4815c0-.4464.5133-.953 1.0043-.953.4911 0 .9938.5066.9938.953v3.4791l2.7653 1.2297c.4139.184.6524.6233.5813 1.0707l-.5152 3.2414h-6.8642l-.4913-1z" fill="#4c5664" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
76
app/client/src/widgets/ButtonWidgetV2/index.tsx
Normal file
76
app/client/src/widgets/ButtonWidgetV2/index.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { ButtonWidget } from "./widget";
|
||||||
|
import IconSVG from "./icon.svg";
|
||||||
|
import { WIDGET_TAGS } from "constants/WidgetConstants";
|
||||||
|
import { ButtonPlacementTypes, RecaptchaTypes } from "components/constants";
|
||||||
|
import { BUTTON_MIN_WIDTH } from "constants/minWidthConstants";
|
||||||
|
import { ResponsiveBehavior } from "utils/autoLayout/constants";
|
||||||
|
import { BUTTON_COLORS, BUTTON_VARIANTS } from "@design-system/widgets";
|
||||||
|
|
||||||
|
export const CONFIG = {
|
||||||
|
type: ButtonWidget.getWidgetType(),
|
||||||
|
name: "Button",
|
||||||
|
iconSVG: IconSVG,
|
||||||
|
needsMeta: false,
|
||||||
|
isCanvas: false,
|
||||||
|
tags: [WIDGET_TAGS.BUTTONS],
|
||||||
|
searchTags: ["click", "submit"],
|
||||||
|
features: {
|
||||||
|
dynamicHeight: {
|
||||||
|
sectionIndex: 0,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
animateLoading: true,
|
||||||
|
text: "Submit",
|
||||||
|
buttonVariant: BUTTON_VARIANTS.FILLED,
|
||||||
|
buttonColor: BUTTON_COLORS.ACCENT,
|
||||||
|
placement: ButtonPlacementTypes.CENTER,
|
||||||
|
rows: 4,
|
||||||
|
columns: 16,
|
||||||
|
widgetName: "Button",
|
||||||
|
isDisabled: false,
|
||||||
|
isVisible: true,
|
||||||
|
isDefaultClickDisabled: true,
|
||||||
|
disabledWhenInvalid: false,
|
||||||
|
resetFormOnClick: false,
|
||||||
|
recaptchaType: RecaptchaTypes.V3,
|
||||||
|
version: 1,
|
||||||
|
responsiveBehavior: ResponsiveBehavior.Hug,
|
||||||
|
minWidth: BUTTON_MIN_WIDTH,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
derived: ButtonWidget.getDerivedPropertiesMap(),
|
||||||
|
default: ButtonWidget.getDefaultPropertiesMap(),
|
||||||
|
meta: ButtonWidget.getMetaPropertiesMap(),
|
||||||
|
contentConfig: ButtonWidget.getPropertyPaneContentConfig(),
|
||||||
|
styleConfig: ButtonWidget.getPropertyPaneStyleConfig(),
|
||||||
|
},
|
||||||
|
autoLayout: {
|
||||||
|
defaults: {
|
||||||
|
rows: 4,
|
||||||
|
columns: 6.453,
|
||||||
|
},
|
||||||
|
autoDimension: {
|
||||||
|
width: true,
|
||||||
|
},
|
||||||
|
widgetSize: [
|
||||||
|
{
|
||||||
|
viewportMinWidth: 0,
|
||||||
|
configuration: () => {
|
||||||
|
return {
|
||||||
|
minWidth: "120px",
|
||||||
|
maxWidth: "360px",
|
||||||
|
minHeight: "40px",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
disableResizeHandles: {
|
||||||
|
horizontal: true,
|
||||||
|
vertical: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ButtonWidget };
|
||||||
145
app/client/src/widgets/ButtonWidgetV2/widget/contentConfig.tsx
Normal file
145
app/client/src/widgets/ButtonWidgetV2/widget/contentConfig.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { RecaptchaTypes } from "components/constants";
|
||||||
|
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
|
||||||
|
import { ValidationTypes } from "constants/WidgetValidation";
|
||||||
|
|
||||||
|
export const propertyPaneContentConfig = [
|
||||||
|
{
|
||||||
|
sectionName: "Basic",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
propertyName: "text",
|
||||||
|
label: "Label",
|
||||||
|
helpText: "Sets the label of the button",
|
||||||
|
controlType: "INPUT_TEXT",
|
||||||
|
placeholderText: "Submit",
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
validation: { type: ValidationTypes.TEXT },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
helpText: "when the button is clicked",
|
||||||
|
propertyName: "onClick",
|
||||||
|
label: "onClick",
|
||||||
|
controlType: "ACTION_SELECTOR",
|
||||||
|
isJSConvertible: true,
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sectionName: "General",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
helpText: "Show helper text with button on hover",
|
||||||
|
propertyName: "tooltip",
|
||||||
|
label: "Tooltip",
|
||||||
|
controlType: "INPUT_TEXT",
|
||||||
|
placeholderText: "Submits Form",
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
validation: { type: ValidationTypes.TEXT },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "isVisible",
|
||||||
|
label: "Visible",
|
||||||
|
helpText: "Controls the visibility of the widget",
|
||||||
|
controlType: "SWITCH",
|
||||||
|
isJSConvertible: true,
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
validation: { type: ValidationTypes.BOOLEAN },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "isDisabled",
|
||||||
|
label: "Disabled",
|
||||||
|
controlType: "SWITCH",
|
||||||
|
helpText: "Disables clicks to this widget",
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sectionName: "Validation",
|
||||||
|
hidden: isAirgapped,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
propertyName: "googleRecaptchaKey",
|
||||||
|
label: "Google reCAPTCHA key",
|
||||||
|
helpText: "Sets Google reCAPTCHA site key for the button",
|
||||||
|
controlType: "INPUT_TEXT",
|
||||||
|
placeholderText: "reCAPTCHA Key",
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
validation: { type: ValidationTypes.TEXT },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
propertyName: "recaptchaType",
|
||||||
|
label: "Google reCAPTCHA version",
|
||||||
|
controlType: "DROP_DOWN",
|
||||||
|
helpText: "Select reCAPTCHA version",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "reCAPTCHA v3",
|
||||||
|
value: RecaptchaTypes.V3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "reCAPTCHA v2",
|
||||||
|
value: RecaptchaTypes.V2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
validation: {
|
||||||
|
type: ValidationTypes.TEXT,
|
||||||
|
params: {
|
||||||
|
allowedValues: [RecaptchaTypes.V3, RecaptchaTypes.V2],
|
||||||
|
default: RecaptchaTypes.V3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// TODO: refactor widgetParentProps implementation when we address #10659
|
||||||
|
{
|
||||||
|
sectionName: "Form settings",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
helpText:
|
||||||
|
"Disabled if the form is invalid, if this widget exists directly within a Form widget.",
|
||||||
|
propertyName: "disabledWhenInvalid",
|
||||||
|
label: "Disabled invalid forms",
|
||||||
|
controlType: "SWITCH",
|
||||||
|
isJSConvertible: true,
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
validation: { type: ValidationTypes.BOOLEAN },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
helpText:
|
||||||
|
"Resets the fields of the form, on click, if this widget exists directly within a Form widget.",
|
||||||
|
propertyName: "resetFormOnClick",
|
||||||
|
label: "Reset form on success",
|
||||||
|
controlType: "SWITCH",
|
||||||
|
isJSConvertible: true,
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
validation: { type: ValidationTypes.BOOLEAN },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
182
app/client/src/widgets/ButtonWidgetV2/widget/index.tsx
Normal file
182
app/client/src/widgets/ButtonWidgetV2/widget/index.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import React from "react";
|
||||||
|
import { toast } from "design-system";
|
||||||
|
|
||||||
|
import BaseWidget from "widgets/BaseWidget";
|
||||||
|
import ButtonComponent from "../component";
|
||||||
|
import { propertyPaneStyleConfig } from "./styleConfig";
|
||||||
|
import type { ButtonComponentProps } from "../component";
|
||||||
|
import type { RecaptchaType } from "components/constants";
|
||||||
|
import type { WidgetType } from "constants/WidgetConstants";
|
||||||
|
import { propertyPaneContentConfig } from "./contentConfig";
|
||||||
|
import type { DerivedPropertiesMap } from "utils/WidgetFactory";
|
||||||
|
import type { WidgetProps, WidgetState } from "widgets/BaseWidget";
|
||||||
|
import type { AutocompletionDefinitions } from "widgets/constants";
|
||||||
|
import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils";
|
||||||
|
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||||
|
import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionConstants";
|
||||||
|
|
||||||
|
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
|
||||||
|
onButtonClickBound: () => void;
|
||||||
|
|
||||||
|
constructor(props: ButtonWidgetProps) {
|
||||||
|
super(props);
|
||||||
|
this.onButtonClickBound = this.onButtonClick.bind(this);
|
||||||
|
this.onRecaptchaSubmitError = this.onRecaptchaSubmitError.bind(this);
|
||||||
|
this.onRecaptchaSubmitSuccess = this.onRecaptchaSubmitSuccess.bind(this);
|
||||||
|
this.state = {
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAutocompleteDefinitions(): AutocompletionDefinitions {
|
||||||
|
return {
|
||||||
|
"!doc":
|
||||||
|
"Buttons are used to capture user intent and trigger actions based on that intent",
|
||||||
|
"!url": "https://docs.appsmith.com/widget-reference/button",
|
||||||
|
isVisible: DefaultAutocompleteDefinitions.isVisible,
|
||||||
|
text: "string",
|
||||||
|
isDisabled: "bool",
|
||||||
|
recaptchaToken: "string",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPropertyPaneContentConfig() {
|
||||||
|
return propertyPaneContentConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPropertyPaneStyleConfig() {
|
||||||
|
return propertyPaneStyleConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getMetaPropertiesMap(): Record<string, any> {
|
||||||
|
return {
|
||||||
|
recaptchaToken: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedPropertiesMap(): DerivedPropertiesMap {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
onButtonClick() {
|
||||||
|
if (this.props.onClick) {
|
||||||
|
this.setState({ isLoading: true });
|
||||||
|
|
||||||
|
super.executeAction({
|
||||||
|
triggerPropertyName: "onClick",
|
||||||
|
dynamicString: this.props.onClick,
|
||||||
|
event: {
|
||||||
|
type: EventType.ON_CLICK,
|
||||||
|
callback: this.handleActionComplete,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.resetFormOnClick && this.props.onReset) {
|
||||||
|
this.props.onReset();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasOnClickAction = () => {
|
||||||
|
const { isDisabled, onClick, onReset, resetFormOnClick } = this.props;
|
||||||
|
|
||||||
|
return Boolean((onClick || onReset || resetFormOnClick) && !isDisabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
onRecaptchaSubmitSuccess(token: string) {
|
||||||
|
this.props.updateWidgetMetaProperty("recaptchaToken", token, {
|
||||||
|
triggerPropertyName: "onClick",
|
||||||
|
dynamicString: this.props.onClick,
|
||||||
|
event: {
|
||||||
|
type: EventType.ON_CLICK,
|
||||||
|
callback: this.handleActionComplete,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRecaptchaSubmitError = (error: string) => {
|
||||||
|
toast.show(error, { kind: "error" });
|
||||||
|
|
||||||
|
if (this.hasOnClickAction()) {
|
||||||
|
this.onButtonClickBound();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRecaptchaV2Loading = (isLoading: boolean) => {
|
||||||
|
if (this.props.onClick) {
|
||||||
|
this.setState({ isLoading });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleActionComplete = (result: ExecutionResult) => {
|
||||||
|
this.setState({
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
if (this.props.resetFormOnClick && this.props.onReset)
|
||||||
|
this.props.onReset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getPageView() {
|
||||||
|
const disabled =
|
||||||
|
this.props.disabledWhenInvalid &&
|
||||||
|
"isFormValid" in this.props &&
|
||||||
|
!this.props.isFormValid;
|
||||||
|
const isDisabled = this.props.isDisabled || disabled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonComponent
|
||||||
|
color={this.props.buttonColor}
|
||||||
|
handleRecaptchaV2Loading={this.handleRecaptchaV2Loading}
|
||||||
|
iconName={this.props.iconName}
|
||||||
|
iconPosition={this.props.iconAlign}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isLoading={this.props.isLoading || this.state.isLoading}
|
||||||
|
key={this.props.widgetId}
|
||||||
|
maxWidth={this.props.maxWidth}
|
||||||
|
minHeight={this.props.minHeight}
|
||||||
|
minWidth={this.props.minWidth}
|
||||||
|
onPress={this.hasOnClickAction() ? this.onButtonClickBound : undefined}
|
||||||
|
onRecaptchaSubmitError={this.onRecaptchaSubmitError}
|
||||||
|
onRecaptchaSubmitSuccess={this.onRecaptchaSubmitSuccess}
|
||||||
|
recaptchaKey={this.props.googleRecaptchaKey}
|
||||||
|
recaptchaType={this.props.recaptchaType}
|
||||||
|
text={this.props.text}
|
||||||
|
tooltip={this.props.tooltip}
|
||||||
|
type={this.props.buttonType || "button"}
|
||||||
|
variant={this.props.buttonVariant}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getWidgetType(): WidgetType {
|
||||||
|
return "BUTTON_WIDGET_V2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonWidgetProps extends WidgetProps {
|
||||||
|
text?: string;
|
||||||
|
isVisible?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
resetFormOnClick?: boolean;
|
||||||
|
googleRecaptchaKey?: string;
|
||||||
|
recaptchaType?: RecaptchaType;
|
||||||
|
disabledWhenInvalid?: boolean;
|
||||||
|
buttonType?: ButtonComponentProps["type"];
|
||||||
|
iconName?: ButtonComponentProps["iconName"];
|
||||||
|
buttonVariant?: ButtonComponentProps["variant"];
|
||||||
|
iconAlign?: ButtonComponentProps["iconPosition"];
|
||||||
|
buttonColor?: ButtonComponentProps["color"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonWidgetState extends WidgetState {
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ButtonWidget };
|
||||||
96
app/client/src/widgets/ButtonWidgetV2/widget/styleConfig.tsx
Normal file
96
app/client/src/widgets/ButtonWidgetV2/widget/styleConfig.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { capitalize } from "lodash";
|
||||||
|
import { BUTTON_COLORS, BUTTON_VARIANTS } from "@design-system/widgets";
|
||||||
|
|
||||||
|
import { ValidationTypes } from "constants/WidgetValidation";
|
||||||
|
|
||||||
|
export const propertyPaneStyleConfig = [
|
||||||
|
{
|
||||||
|
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(BUTTON_COLORS).map((semantic) => ({
|
||||||
|
label: capitalize(semantic),
|
||||||
|
value: semantic,
|
||||||
|
})),
|
||||||
|
isJSConvertible: true,
|
||||||
|
isBindProperty: true,
|
||||||
|
isTriggerProperty: false,
|
||||||
|
validation: {
|
||||||
|
type: ValidationTypes.TEXT,
|
||||||
|
params: {
|
||||||
|
allowedValues: Object.values(BUTTON_COLORS),
|
||||||
|
default: BUTTON_COLORS.ACCENT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sectionName: "Icon",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
propertyName: "iconName",
|
||||||
|
label: "Select icon",
|
||||||
|
helpText: "Sets the icon to be used for the button",
|
||||||
|
controlType: "ICON_SELECT",
|
||||||
|
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"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
Loading…
Reference in New Issue
Block a user