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/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",
|
||||
"^proxy-memoize$": "<rootDir>/node_modules/proxy-memoize/dist/wrapper.cjs",
|
||||
// @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 { Tooltip, TooltipTrigger, TooltipContent } from "./";
|
||||
import { TooltipRoot, TooltipTrigger, TooltipContent } from "./";
|
||||
import { Button } from "../Button";
|
||||
|
||||
<Meta
|
||||
title="Design-system/headless/Tooltip"
|
||||
component={Tooltip}
|
||||
component={TooltipRoot}
|
||||
args={{
|
||||
open: undefined,
|
||||
onOpenChange: undefined,
|
||||
|
|
@ -14,12 +14,12 @@ import { Button } from "../Button";
|
|||
|
||||
export const Template = (args) => {
|
||||
return (
|
||||
<Tooltip {...args}>
|
||||
<TooltipRoot {...args}>
|
||||
<TooltipTrigger>
|
||||
<button>My trigger</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipRoot>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -37,30 +37,30 @@ The placement of the tooltip can be changed by passing the `placement` prop.
|
|||
|
||||
<Canvas>
|
||||
<Story name="Tooltip placement">
|
||||
<Tooltip placement="left">
|
||||
<TooltipRoot placement="left">
|
||||
<TooltipTrigger>
|
||||
<button>Left</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top">
|
||||
</TooltipRoot>
|
||||
<TooltipRoot placement="top">
|
||||
<TooltipTrigger>
|
||||
<button>Top</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom">
|
||||
</TooltipRoot>
|
||||
<TooltipRoot placement="bottom">
|
||||
<TooltipTrigger>
|
||||
<button>Bottom</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip placement="right">
|
||||
</TooltipRoot>
|
||||
<TooltipRoot placement="right">
|
||||
<TooltipTrigger>
|
||||
<button>Right</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipRoot>
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
|
|
@ -70,12 +70,12 @@ If the trigger is disabled, the tooltip will not be displayed.
|
|||
|
||||
<Canvas>
|
||||
<Story name="Tooltip disabled">
|
||||
<Tooltip>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger>
|
||||
<Button isDisabled>Disabled</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent disabled>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipRoot>
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { useTooltip } from "./useTooltip";
|
|||
import { TooltipContext } from "./TooltipContext";
|
||||
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);
|
||||
|
||||
return (
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export { Tooltip } from "./Tooltip";
|
||||
export { TooltipRoot } from "./TooltipRoot";
|
||||
export { TooltipTrigger } from "./TooltipTrigger";
|
||||
export { TooltipContent } from "./TooltipContent";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./ThemeContext";
|
||||
export * from "./ThemeProvider";
|
||||
export * from "./types";
|
||||
export * from "./useTheme";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import type { ReactNode } from "react";
|
||||
|
||||
import type { ColorMode } from "../color";
|
||||
import type { FontFamily } from "../typography";
|
||||
import type { RootUnit, ThemeToken } from "../token";
|
||||
|
||||
export type Theme = ThemeToken & {
|
||||
|
|
@ -15,3 +18,11 @@ export interface ThemeProviderProps {
|
|||
children: ReactNode;
|
||||
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 spacing?: TokenObj;
|
||||
private sizing?: TokenObj;
|
||||
private zIndex?: TokenObj;
|
||||
|
||||
constructor({
|
||||
borderRadius,
|
||||
|
|
@ -37,6 +38,7 @@ export class TokensAccessor {
|
|||
sizing,
|
||||
spacing,
|
||||
typography,
|
||||
zIndex,
|
||||
}: TokenSource) {
|
||||
this.seedColor = seedColor;
|
||||
this.colorMode = colorMode;
|
||||
|
|
@ -49,6 +51,7 @@ export class TokensAccessor {
|
|||
this.sizing = sizing;
|
||||
this.spacing = spacing;
|
||||
this.typography = typography;
|
||||
this.zIndex = zIndex;
|
||||
}
|
||||
|
||||
updateRootUnit = (rootUnit: RootUnit) => {
|
||||
|
|
@ -87,6 +90,10 @@ export class TokensAccessor {
|
|||
this.opacity = opacity;
|
||||
};
|
||||
|
||||
updateZIndex = (zIndex: TokenObj) => {
|
||||
this.zIndex = zIndex;
|
||||
};
|
||||
|
||||
updateSpacing = (spacing: TokenObj) => {
|
||||
this.spacing = spacing;
|
||||
};
|
||||
|
|
@ -107,6 +114,7 @@ export class TokensAccessor {
|
|||
...this.getBoxShadow(),
|
||||
...this.getBorderWidth(),
|
||||
...this.getOpacity(),
|
||||
...this.getZIndex(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -178,6 +186,12 @@ export class TokensAccessor {
|
|||
return this.createTokenObject(this.opacity, "opacity");
|
||||
};
|
||||
|
||||
getZIndex = () => {
|
||||
if (this.zIndex == null) return {} as ThemeToken;
|
||||
|
||||
return this.createTokenObject(this.zIndex, "zIndex");
|
||||
};
|
||||
|
||||
private get isLightMode() {
|
||||
return this.colorMode === "light";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,5 +47,11 @@
|
|||
},
|
||||
"opacity": {
|
||||
"disabled": 0.3
|
||||
},
|
||||
"zIndex": {
|
||||
"1": 3,
|
||||
"2": 4,
|
||||
"3": 10,
|
||||
"99": 9999
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ export type TokenType =
|
|||
| "borderRadius"
|
||||
| "boxShadow"
|
||||
| "borderWidth"
|
||||
| "opacity";
|
||||
| "opacity"
|
||||
| "zIndex";
|
||||
|
||||
export interface Token {
|
||||
value: string | number;
|
||||
|
|
@ -31,6 +32,7 @@ export interface TokenSource {
|
|||
borderWidth?: TokenObj;
|
||||
opacity?: TokenObj;
|
||||
fontFamily?: FontFamily;
|
||||
zIndex?: TokenObj;
|
||||
sizing?: TokenObj;
|
||||
spacing?: TokenObj;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useVisuallyHidden } from "@react-aria/visually-hidden";
|
|||
|
||||
import { Text } from "../Text";
|
||||
import { Spinner } from "../Spinner";
|
||||
import type { ButtonColor, ButtonIconPosition, ButtonVariant } from "./types";
|
||||
import { DragContainer, StyledButton } from "./index.styled";
|
||||
|
||||
export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
|
||||
|
|
@ -15,9 +16,9 @@ export interface ButtonProps extends Omit<HeadlessButtonProps, "className"> {
|
|||
*
|
||||
* @default "filled"
|
||||
*/
|
||||
variant?: "filled" | "outlined" | "ghost";
|
||||
variant?: ButtonVariant;
|
||||
/** Color tone of the button */
|
||||
color?: "accent" | "neutral" | "positive" | "negative" | "warning";
|
||||
color?: ButtonColor;
|
||||
/** When true, makes the button occupy all the space available */
|
||||
isFitContainer?: boolean;
|
||||
/** 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?: React.ReactNode;
|
||||
/** Indicates the position of icon of the button */
|
||||
iconPosition?: "start" | "end";
|
||||
/** Makes the button visually and functionally disabled but focusable */
|
||||
iconPosition?: ButtonIconPosition;
|
||||
/** Makes the button visually and functionaly disabled but focusable */
|
||||
visuallyDisabled?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +65,9 @@ export const Button = forwardRef(
|
|||
return (
|
||||
<>
|
||||
{icon}
|
||||
<Text lineClamp={1}>{children}</Text>
|
||||
<Text lineClamp={1} textAlign="center">
|
||||
{children}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -80,7 +83,7 @@ export const Button = forwardRef(
|
|||
data-button=""
|
||||
data-color={color}
|
||||
data-fit-container={isFitContainer ? "" : undefined}
|
||||
data-icon-position={iconPosition === "start" ? undefined : "end"}
|
||||
data-icon-position={iconPosition === "start" ? "start" : "end"}
|
||||
data-loading={isLoading ? "" : undefined}
|
||||
data-variant={variant}
|
||||
draggable
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import styled, { css } from "styled-components";
|
||||
import { Button as HeadlessButton } from "@design-system/headless";
|
||||
import type { PickRename } from "../../utils";
|
||||
|
||||
import type { ButtonProps } from "./Button";
|
||||
import type { PickRename } from "../../utils";
|
||||
|
||||
type StyledButtonProps = PickRename<
|
||||
ButtonProps,
|
||||
|
|
@ -73,16 +73,19 @@ export const StyledButton = styled(HeadlessButton)<StyledButtonProps>`
|
|||
cursor: pointer;
|
||||
outline: 0;
|
||||
padding: var(--spacing-2) var(--spacing-4);
|
||||
block-size: var(--sizing-8);
|
||||
border-radius: var(--border-radius-1);
|
||||
user-select: none;
|
||||
height: var(--sizing-8);
|
||||
min-width: var(--sizing-8);
|
||||
text-align: center;
|
||||
min-inline-size: var(--sizing-8);
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
|
||||
& *:not([data-hidden]) + *:not([data-hidden]) {
|
||||
margin: 0 var(--spacing-1);
|
||||
&[data-icon-position="start"] *:not([data-hidden]) + *:not([data-hidden]) {
|
||||
margin-inline-start: var(--spacing-1);
|
||||
}
|
||||
|
||||
&[data-icon-position="end"] *:not([data-hidden]) + *:not([data-hidden]) {
|
||||
margin-inline-end: var(--spacing-1);
|
||||
}
|
||||
|
||||
${buttonStyles}
|
||||
|
|
@ -96,8 +99,14 @@ export const StyledButton = styled(HeadlessButton)<StyledButtonProps>`
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: var(--sizing-5);
|
||||
width: var(--sizing-5);
|
||||
height: var(--sizing-4);
|
||||
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 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-style: ${({ $isItalic }) => ($isItalic ? "italic" : "normal")};
|
||||
text-align: ${({ $textAlign }) => $textAlign};
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
|
||||
color: ${({ color }) => {
|
||||
switch (true) {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
|||
|
||||
import { Button } from "../Button";
|
||||
import { ButtonGroup } from "../ButtonGroup";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "./";
|
||||
import { TooltipRoot, TooltipTrigger, TooltipContent } from "./";
|
||||
|
||||
<Meta
|
||||
title="Design-system/widgets/Tooltip"
|
||||
component={Tooltip}
|
||||
component={TooltipRoot}
|
||||
args={{
|
||||
open: undefined,
|
||||
onOpenChange: undefined,
|
||||
|
|
@ -15,12 +15,12 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "./";
|
|||
|
||||
export const Template = (args) => {
|
||||
return (
|
||||
<Tooltip {...args}>
|
||||
<TooltipRoot {...args}>
|
||||
<TooltipTrigger>
|
||||
<Button>My trigger</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipRoot>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -41,30 +41,30 @@ The placement of the tooltip can be changed by passing the `placement` prop.
|
|||
<Canvas>
|
||||
<Story name="Tooltip placement">
|
||||
<ButtonGroup>
|
||||
<Tooltip placement="left">
|
||||
<TooltipRoot placement="left">
|
||||
<TooltipTrigger>
|
||||
<Button variant="outlined">Left</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top">
|
||||
</TooltipRoot>
|
||||
<TooltipRoot placement="top">
|
||||
<TooltipTrigger>
|
||||
<Button variant="outlined">Top</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom">
|
||||
</TooltipRoot>
|
||||
<TooltipRoot placement="bottom">
|
||||
<TooltipTrigger>
|
||||
<Button variant="outlined">Bottom</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip placement="right">
|
||||
</TooltipRoot>
|
||||
<TooltipRoot placement="right">
|
||||
<TooltipTrigger>
|
||||
<Button variant="outlined">Right</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipRoot>
|
||||
</ButtonGroup>
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
|
@ -75,7 +75,7 @@ If the trigger is disabled, the tooltip will still be displayed.
|
|||
|
||||
<Canvas>
|
||||
<Story name="Tooltip disabled">
|
||||
<Tooltip>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
isDisabled
|
||||
|
|
@ -87,6 +87,16 @@ If the trigger is disabled, the tooltip will still be displayed.
|
|||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>My tooltip</TooltipContent>
|
||||
</TooltipRoot>
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
# Tooltip Component
|
||||
|
||||
<Canvas>
|
||||
<Story name="Tooltip Component">
|
||||
<Tooltip>
|
||||
<Button tooltip="Tooltip">My trigger</Button>
|
||||
</Tooltip>
|
||||
</Story>
|
||||
</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);
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border-radius: var(--border-radius-1);
|
||||
z-index: var(--z-index-99);
|
||||
|
||||
[data-tooltip-trigger-arrow] {
|
||||
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 { TooltipContent } from "./TooltipContent";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// components
|
||||
export { Icon } from "@design-system/headless";
|
||||
|
||||
export * from "./components/Button";
|
||||
export * from "./components/Checkbox";
|
||||
export * from "./components/Text";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
Text,
|
||||
CheckboxGroup,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
TooltipRoot,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from "../";
|
||||
|
|
@ -34,14 +34,14 @@ export const ComplexForm = () => {
|
|||
gap: "var(--spacing-2)",
|
||||
}}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outlined">Cancel</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
If you cancel, you will lose your order
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipRoot>
|
||||
<Button>Ok</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,5 @@ export type OmitRename<
|
|||
TOmitKeys extends keyof TObj,
|
||||
TSymbol extends string = "$",
|
||||
> = {
|
||||
[K in keyof TObj as K extends TOmitKeys
|
||||
? never
|
||||
: `${TSymbol}${string & K}`]: TObj[K];
|
||||
} & Omit<TObj, TOmitKeys>;
|
||||
[K in keyof Omit<TObj, TOmitKeys> as `${TSymbol}${string & K}`]: TObj[K];
|
||||
} & Pick<TObj, TOmitKeys>;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
TokensAccessor,
|
||||
defaultTokens,
|
||||
useFluidTokens,
|
||||
useTheme,
|
||||
} from "@design-system/theming";
|
||||
import Color from "colorjs.io";
|
||||
|
||||
|
|
@ -22,132 +23,14 @@ const StyledThemeProvider = styled(ThemeProvider)`
|
|||
`;
|
||||
|
||||
export const theming = (Story, args) => {
|
||||
const [rootUnitRatio, setRootUnitRatio] = useState(1);
|
||||
const { fluid, ...restDefaultTokens } = defaultTokens;
|
||||
|
||||
const { typography, rootUnit, spacing, sizing } = useFluidTokens(
|
||||
fluid,
|
||||
rootUnitRatio,
|
||||
);
|
||||
|
||||
const tokensAccessor = new TokensAccessor({
|
||||
...restDefaultTokens,
|
||||
rootUnit,
|
||||
spacing,
|
||||
sizing,
|
||||
typography,
|
||||
const { theme } = useTheme({
|
||||
seedColor: args.globals.accentColor,
|
||||
colorMode: args.globals.colorMode,
|
||||
borderRadius: args.globals.borderRadius,
|
||||
fontFamily: args.globals.fontFamily,
|
||||
rootUnitRatio: args.globals.rootUnitRatio,
|
||||
});
|
||||
|
||||
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 (
|
||||
<StyledThemeProvider theme={theme}>
|
||||
<Story />
|
||||
|
|
|
|||
|
|
@ -15,3 +15,14 @@ body,
|
|||
.innerZoomElementWrapper > * {
|
||||
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_binding_enabled: "ab_ds_binding_enabled",
|
||||
release_scim_provisioning_enabled: "release_scim_provisioning_enabled",
|
||||
ab_wds_enabled: "ab_wds_enabled",
|
||||
release_widgetdiscovery_enabled: "release_widgetdiscovery_enabled",
|
||||
} as const;
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
|
|||
ab_ds_schema_enabled: false,
|
||||
ab_ds_binding_enabled: false,
|
||||
release_scim_provisioning_enabled: false,
|
||||
ab_wds_enabled: false,
|
||||
release_widgetdiscovery_enabled: false,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createSelector } from "reselect";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
import type { FeatureFlag } from "@appsmith/entities/FeatureFlag";
|
||||
import { createSelector } from "reselect";
|
||||
|
||||
export const selectFeatureFlags = (state: AppState) =>
|
||||
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 useWidgetFocus from "utils/hooks/useWidgetFocus/useWidgetFocus";
|
||||
import HtmlTitle from "./AppViewerHtmlTitle";
|
||||
import BottomBar from "components/BottomBar";
|
||||
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
|
||||
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
|
||||
import { editorInitializer } from "../../utils/editor/EditorUtils";
|
||||
import { widgetInitialisationSuccess } from "../../actions/widgetActions";
|
||||
import BottomBar from "components/BottomBar";
|
||||
import { areEnvironmentsFetched } from "@appsmith/selectors/environmentSelectors";
|
||||
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<{
|
||||
hasPages: boolean;
|
||||
|
|
@ -98,7 +103,10 @@ function AppViewer(props: Props) {
|
|||
const currentApplicationDetails: ApplicationPayload | undefined = useSelector(
|
||||
getCurrentApplication,
|
||||
);
|
||||
|
||||
const { theme } = useTheme({
|
||||
borderRadius: selectedTheme.properties.borderRadius.appBorderRadius,
|
||||
seedColor: selectedTheme.properties.colors.primaryColor,
|
||||
});
|
||||
const focusRef = useWidgetFocus();
|
||||
|
||||
const workspaceId = currentApplicationDetails?.workspaceId || "";
|
||||
|
|
@ -176,46 +184,53 @@ function AppViewer(props: Props) {
|
|||
};
|
||||
}, [selectedTheme.properties.fontFamily.appFont]);
|
||||
|
||||
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
|
||||
const backgroundForBody = isWDSV2Enabled
|
||||
? "var(--color-bg)"
|
||||
: selectedTheme.properties.colors.backgroundColor;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<EditorContextProvider renderMode="PAGE">
|
||||
<WidgetGlobaStyles
|
||||
fontFamily={selectedTheme.properties.fontFamily.appFont}
|
||||
primaryColor={selectedTheme.properties.colors.primaryColor}
|
||||
/>
|
||||
<HtmlTitle
|
||||
description={pageDescription}
|
||||
name={currentApplicationDetails?.name}
|
||||
/>
|
||||
<AppViewerBodyContainer
|
||||
backgroundColor={selectedTheme.properties.colors.backgroundColor}
|
||||
>
|
||||
<AppViewerBody
|
||||
className={CANVAS_SELECTOR}
|
||||
hasPages={pages.length > 1}
|
||||
headerHeight={headerHeight}
|
||||
ref={focusRef}
|
||||
showBottomBar={showBottomBar}
|
||||
showGuidedTourMessage={showGuidedTourMessage}
|
||||
>
|
||||
{isInitialized && <AppViewerPageContainer />}
|
||||
</AppViewerBody>
|
||||
{showBottomBar && <BottomBar viewMode />}
|
||||
{!hideWatermark && (
|
||||
<a
|
||||
className={`fixed hidden right-8 ${
|
||||
showBottomBar ? "bottom-12" : "bottom-4"
|
||||
} z-3 hover:no-underline md:flex`}
|
||||
href="https://appsmith.com"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<BrandingBadge />
|
||||
</a>
|
||||
<WDSThemeProvider theme={theme}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<EditorContextProvider renderMode="PAGE">
|
||||
{!isWDSV2Enabled && (
|
||||
<WidgetGlobaStyles
|
||||
fontFamily={selectedTheme.properties.fontFamily.appFont}
|
||||
primaryColor={selectedTheme.properties.colors.primaryColor}
|
||||
/>
|
||||
)}
|
||||
</AppViewerBodyContainer>
|
||||
</EditorContextProvider>
|
||||
</ThemeProvider>
|
||||
<HtmlTitle
|
||||
description={pageDescription}
|
||||
name={currentApplicationDetails?.name}
|
||||
/>
|
||||
<AppViewerBodyContainer backgroundColor={backgroundForBody}>
|
||||
<AppViewerBody
|
||||
className={CANVAS_SELECTOR}
|
||||
hasPages={pages.length > 1}
|
||||
headerHeight={headerHeight}
|
||||
ref={focusRef}
|
||||
showBottomBar={showBottomBar}
|
||||
showGuidedTourMessage={showGuidedTourMessage}
|
||||
>
|
||||
{isInitialized && <AppViewerPageContainer />}
|
||||
</AppViewerBody>
|
||||
{showBottomBar && <BottomBar viewMode />}
|
||||
{!hideWatermark && (
|
||||
<a
|
||||
className={`fixed hidden right-8 ${
|
||||
showBottomBar ? "bottom-12" : "bottom-4"
|
||||
} z-3 hover:no-underline md:flex`}
|
||||
href="https://appsmith.com"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<BrandingBadge />
|
||||
</a>
|
||||
)}
|
||||
</AppViewerBodyContainer>
|
||||
</EditorContextProvider>
|
||||
</ThemeProvider>
|
||||
</WDSThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
import * as Sentry from "@sentry/react";
|
||||
import log from "loglevel";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import WidgetFactory from "utils/WidgetFactory";
|
||||
import type { CanvasWidgetStructure } from "widgets/constants";
|
||||
|
||||
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 { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { previewModeSelector } from "selectors/editorSelectors";
|
||||
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
|
||||
import { getViewportClassName } from "utils/autoLayout/AutoLayoutUtils";
|
||||
import {
|
||||
ThemeProvider as WDSThemeProvider,
|
||||
useTheme,
|
||||
} from "@design-system/theming";
|
||||
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
|
||||
|
||||
interface CanvasProps {
|
||||
|
|
@ -36,6 +41,11 @@ const Canvas = (props: CanvasProps) => {
|
|||
getIsAppSettingsPaneWithNavigationTabOpen,
|
||||
);
|
||||
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
|
||||
|
|
@ -43,9 +53,17 @@ const Canvas = (props: CanvasProps) => {
|
|||
let backgroundForCanvas;
|
||||
|
||||
if (isPreviewMode || isAppSettingsPaneWithNavigationTabOpen) {
|
||||
backgroundForCanvas = "initial";
|
||||
if (isWDSV2Enabled) {
|
||||
backgroundForCanvas = "var(--color-bg)";
|
||||
} else {
|
||||
backgroundForCanvas = "initial";
|
||||
}
|
||||
} else {
|
||||
backgroundForCanvas = selectedTheme.properties.colors.backgroundColor;
|
||||
if (isWDSV2Enabled) {
|
||||
backgroundForCanvas = "var(--color-bg)";
|
||||
} else {
|
||||
backgroundForCanvas = selectedTheme.properties.colors.backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
const focusRef = useWidgetFocus();
|
||||
|
|
@ -54,23 +72,25 @@ const Canvas = (props: CanvasProps) => {
|
|||
const paddingBottomClass = props.isAutoLayout ? "" : "pb-52";
|
||||
try {
|
||||
return (
|
||||
<Container
|
||||
$isAutoLayout={!!props.isAutoLayout}
|
||||
background={backgroundForCanvas}
|
||||
className={`relative t--canvas-artboard ${paddingBottomClass} ${marginHorizontalClass} ${getViewportClassName(
|
||||
canvasWidth,
|
||||
)}`}
|
||||
data-testid="t--canvas-artboard"
|
||||
id="art-board"
|
||||
ref={focusRef}
|
||||
width={canvasWidth}
|
||||
>
|
||||
{props.widgetsStructure.widgetId &&
|
||||
WidgetFactory.createWidget(
|
||||
props.widgetsStructure,
|
||||
RenderModes.CANVAS,
|
||||
)}
|
||||
</Container>
|
||||
<WDSThemeProvider theme={theme}>
|
||||
<Container
|
||||
$isAutoLayout={!!props.isAutoLayout}
|
||||
background={backgroundForCanvas}
|
||||
className={`relative t--canvas-artboard ${paddingBottomClass} ${marginHorizontalClass} ${getViewportClassName(
|
||||
canvasWidth,
|
||||
)}`}
|
||||
data-testid="t--canvas-artboard"
|
||||
id="art-board"
|
||||
ref={focusRef}
|
||||
width={canvasWidth}
|
||||
>
|
||||
{props.widgetsStructure.widgetId &&
|
||||
WidgetFactory.createWidget(
|
||||
props.widgetsStructure,
|
||||
RenderModes.CANVAS,
|
||||
)}
|
||||
</Container>
|
||||
</WDSThemeProvider>
|
||||
);
|
||||
} catch (error) {
|
||||
log.error("Error rendering DSL", error);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export const excludeList: WidgetType[] = [
|
|||
"FILE_PICKER_WIDGET",
|
||||
"FILE_PICKER_WIDGET_V2",
|
||||
"TABLE_WIDGET_V2",
|
||||
"BUTTON_WIDGET_V2",
|
||||
];
|
||||
|
||||
function PropertyPaneView(
|
||||
|
|
@ -184,7 +185,7 @@ function PropertyPaneView(
|
|||
|
||||
return (
|
||||
<div
|
||||
className="w-full overflow-y-scroll h-full"
|
||||
className="w-full h-full overflow-y-scroll"
|
||||
key={`property-pane-${widgetProperties.widgetId}`}
|
||||
ref={containerRef}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -25,15 +25,16 @@ import {
|
|||
getSelectedAppTheme,
|
||||
} from "selectors/appThemingSelectors";
|
||||
import { getIsAutoLayout } from "selectors/canvasSelectors";
|
||||
import { getCanvasWidgetsStructure } from "selectors/entitiesSelector";
|
||||
import { getCurrentThemeDetails } from "selectors/themeSelectors";
|
||||
import { getCanvasWidgetsStructure } from "selectors/entitiesSelector";
|
||||
import {
|
||||
AUTOLAYOUT_RESIZER_WIDTH_BUFFER,
|
||||
useDynamicAppLayout,
|
||||
} from "utils/hooks/useDynamicAppLayout";
|
||||
import Canvas from "../Canvas";
|
||||
import { CanvasResizer } from "widgets/CanvasResizer";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
import { CanvasResizer } from "widgets/CanvasResizer";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { getIsAnonymousDataPopupVisible } from "selectors/onboardingSelectors";
|
||||
|
||||
type CanvasContainerProps = {
|
||||
|
|
@ -119,9 +120,9 @@ function CanvasContainer(props: CanvasContainerProps) {
|
|||
const isAppThemeChanging = useSelector(getAppThemeIsChanging);
|
||||
const showCanvasTopSection = useSelector(showCanvasTopSectionSelector);
|
||||
const showAnonymousDataPopup = useSelector(getIsAnonymousDataPopupVisible);
|
||||
|
||||
const isLayoutingInitialized = useDynamicAppLayout();
|
||||
const isPageInitializing = isFetchingPage || !isLayoutingInitialized;
|
||||
const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled");
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -184,7 +185,9 @@ function CanvasContainer(props: CanvasContainerProps) {
|
|||
$isAutoLayout={isAutoLayout}
|
||||
background={
|
||||
isPreviewMode || isAppSettingsPaneWithNavigationTabOpen
|
||||
? selectedTheme.properties.colors.backgroundColor
|
||||
? isWDSV2Enabled
|
||||
? "var(--bg-color)"
|
||||
: selectedTheme.properties.colors.backgroundColor
|
||||
: "initial"
|
||||
}
|
||||
className={classNames({
|
||||
|
|
@ -214,10 +217,12 @@ function CanvasContainer(props: CanvasContainerProps) {
|
|||
pointerEvents: isAutoCanvasResizing ? "none" : "auto",
|
||||
}}
|
||||
>
|
||||
<WidgetGlobaStyles
|
||||
fontFamily={selectedTheme.properties.fontFamily.appFont}
|
||||
primaryColor={selectedTheme.properties.colors.primaryColor}
|
||||
/>
|
||||
{!isWDSV2Enabled && (
|
||||
<WidgetGlobaStyles
|
||||
fontFamily={selectedTheme.properties.fontFamily.appFont}
|
||||
primaryColor={selectedTheme.properties.colors.primaryColor}
|
||||
/>
|
||||
)}
|
||||
{isAppThemeChanging && (
|
||||
<div className="fixed top-0 bottom-0 left-0 right-0 flex items-center justify-center bg-white/70 z-[2]">
|
||||
<Spinner size="md" />
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const initialState: AppThemingState = {
|
|||
properties: {
|
||||
colors: {
|
||||
backgroundColor: "#F8FAFC",
|
||||
primaryColor: "",
|
||||
primaryColor: "#000",
|
||||
secondaryColor: "",
|
||||
},
|
||||
borderRadius: {},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import type {
|
|||
AppLayoutConfig,
|
||||
PageListReduxState,
|
||||
} from "reducers/entityReducers/pageListReducer";
|
||||
import type { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer";
|
||||
import type { WidgetCardProps, WidgetProps } from "widgets/BaseWidget";
|
||||
|
||||
import type { Page } from "@appsmith/constants/ReduxActionConstants";
|
||||
|
|
@ -61,6 +60,9 @@ import WidgetFactory from "utils/WidgetFactory";
|
|||
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
|
||||
import { nestDSL } from "@shared/dsl";
|
||||
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) =>
|
||||
state.ui.widgetDragResize.isResizing || state.ui.widgetDragResize.isDragging;
|
||||
|
|
@ -336,12 +338,29 @@ export const getCurrentPageName = createSelector(
|
|||
export const getWidgetCards = createSelector(
|
||||
getWidgetConfigs,
|
||||
getIsAutoLayout,
|
||||
(widgetConfigs: WidgetConfigReducerState, isAutoLayout: boolean) => {
|
||||
(_state) => selectFeatureFlagCheck(_state, FEATURE_FLAG.ab_wds_enabled),
|
||||
(widgetConfigs, isAutoLayout, isWDSEnabled) => {
|
||||
const cards = Object.values(widgetConfigs.config).filter((config) => {
|
||||
return isAirgapped()
|
||||
? config.widgetName !== "Map" && !config.hideCard
|
||||
: !config.hideCard;
|
||||
if (isAirgapped()) {
|
||||
return config.widgetName !== "Map" && !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 {
|
||||
detachFromLayout = false,
|
||||
|
|
@ -374,6 +393,7 @@ export const getWidgetCards = createSelector(
|
|||
};
|
||||
});
|
||||
const sortedCards = sortBy(_cards, ["displayName"]);
|
||||
|
||||
return sortedCards;
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -164,6 +164,10 @@ import CodeScannerWidget, {
|
|||
import ListWidgetV2, {
|
||||
CONFIG as LIST_WIDGET_CONFIG_V2,
|
||||
} from "widgets/ListWidgetV2";
|
||||
import {
|
||||
ButtonWidget as ButtonWidgetV2,
|
||||
CONFIG as BUTTON_WIDGET_CONFIG_V2,
|
||||
} from "widgets/ButtonWidgetV2";
|
||||
|
||||
export const ALL_WIDGETS_AND_CONFIG: [any, WidgetConfiguration][] = [
|
||||
[CanvasWidget, CANVAS_WIDGET_CONFIG],
|
||||
|
|
@ -215,6 +219,7 @@ export const ALL_WIDGETS_AND_CONFIG: [any, WidgetConfiguration][] = [
|
|||
[CategorySliderWidget, CATEGORY_SLIDER_WIDGET_CONFIG],
|
||||
[CodeScannerWidget, CODE_SCANNER_WIDGET_CONFIG],
|
||||
[ListWidgetV2, LIST_WIDGET_CONFIG_V2],
|
||||
[ButtonWidgetV2, BUTTON_WIDGET_CONFIG_V2],
|
||||
|
||||
//Deprecated Widgets
|
||||
[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