chore: Add new WDS statbox (#30744)

Fixes #30423 

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced the `StatBoxComponent` for displaying statistical
information with flexibility in presentation, including icons, labels,
values, and sublabels.
- Expanded the application's widget offerings by adding the
`WDSStatBoxWidget`.
- **Enhancements**
- Improved flexibility and customization of the Flex component with the
introduction of the `isInner` property.
- Enhanced feature flag checks for quicker enablement of specific
functionalities.
- **Documentation**
- Added comprehensive configurations and documentation for the
`WDSStatBoxWidget`, including property pane settings, default
configurations, and meta information.
- **Style**
- Implemented new styling for the `StatBoxComponent`, defining its
appearance including border, width, and background.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Pawan Kumar 2024-02-01 16:47:23 +05:30 committed by GitHub
parent 67d20d9858
commit 7eda27b140
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 420 additions and 36 deletions

View File

@ -80,6 +80,7 @@
},
"iconSize": {
"1": "16px",
"2": "24px"
"2": "24px",
"3": "32px"
}
}

View File

@ -33,7 +33,7 @@ export interface ActionGroupProps<T>
>,
InheritedActionButtonProps {
orientation?: keyof typeof ACTION_GROUP_ORIENTATIONS;
size?: keyof typeof SIZES;
size?: Omit<keyof typeof SIZES, "large">;
}
export interface ActionGroupItemProps<T> extends ButtonProps {

View File

@ -39,14 +39,16 @@ export const Template = (args) => {
<Story name="Size">
<Flex gap="spacing-2" direction="column">
{Object.keys(SIZES).map((size) => (
<ActionGroup size={size} key={size}>
<Item key="option-1">Option 1</Item>
<Item key="option-2">Option 2</Item>
<Item key="option-3">Option 3</Item>
<Item key="option-4">Option 4</Item>
</ActionGroup>
))}
{Object.keys(SIZES)
.filter((size) => !["large"].includes(size))
.map((size) => (
<ActionGroup size={size} key={size}>
<Item key="option-1">Option 1</Item>
<Item key="option-2">Option 2</Item>
<Item key="option-3">Option 3</Item>
<Item key="option-4">Option 4</Item>
</ActionGroup>
))}
</Flex>
</Story>

View File

@ -41,5 +41,5 @@ export interface ButtonProps extends HeadlessButtonProps {
/** Size of the button
* @default medium
*/
size?: keyof typeof SIZES;
size?: Omit<keyof typeof SIZES, "large">;
}

View File

@ -82,11 +82,13 @@ There are 2 sizes of the button component. Default size is `medium`.
<Story name="Size">
<Flex gap="spacing-2">
{Object.keys(SIZES).map((size) => (
<div key={size} style={{ marginBottom: 10 }}>
<Button size={size} icon="star" children={size} />
</div>
))}
{Object.keys(SIZES)
.filter((size) => !["large"].includes(size))
.map((size) => (
<div key={size} style={{ marginBottom: 10 }}>
<Button size={size} icon="star" children={size} />
</div>
))}
</Flex>
</Story>

View File

@ -1,13 +1,15 @@
import { css } from "@emotion/css";
import kebabCase from "lodash/kebabCase";
import type { FlexCssProps, CssVarValues } from "./types";
import type { FlexCssProps, CssVarValues, FlexProps } from "./types";
export const flexCss = (props: FlexCssProps) => {
const { isInner, ...rest } = props;
return css`
${Object.keys(props).reduce(
${Object.keys(rest).reduce(
(styles, key) =>
styles + flexStyles(key, props[key as keyof FlexCssProps]),
styles + flexStyles(key, props[key as keyof FlexCssProps], { isInner }),
"",
)}
`;
@ -16,6 +18,7 @@ export const flexCss = (props: FlexCssProps) => {
const flexStyles = (
cssProp: string,
value: FlexCssProps[keyof FlexCssProps],
extraProps?: Pick<FlexProps, "isInner">,
): string => {
if (value == null) return "";
@ -71,6 +74,7 @@ const flexStyles = (
kebabCase(cssProp),
value as CssVarValues,
cssVarValue,
extraProps,
)};
`;
default:
@ -83,7 +87,8 @@ const flexStyles = (
export const containerDimensionStyles = <T = FlexCssProps[keyof FlexCssProps]>(
cssProp: string,
value: T,
callback?: (value: T) => void,
callback?: (value: T, extraProps?: Pick<FlexProps, "isInner">) => void,
extraProps?: Pick<FlexProps, "isInner">,
) => {
if (value == null) return;
@ -95,20 +100,22 @@ export const containerDimensionStyles = <T = FlexCssProps[keyof FlexCssProps]>(
`@container (min-width: ${current}) {& {
${cssProp}: ${
//@ts-expect-error: type mismatch
callback ? callback(value[current]) : value[current]
callback ? callback(value[current], extraProps) : value[current]
};}}`
);
} else {
return (
prev +
//@ts-expect-error: type mismatch
`${cssProp}: ${callback ? callback(value[current]) : value[current]};`
`${cssProp}: ${
//@ts-expect-error: type mismatch
callback ? callback(value[current], extraProps) : value[current]
};`
);
}
}, "");
}
return `${cssProp}: ${callback ? callback(value) : value};`;
return `${cssProp}: ${callback ? callback(value, extraProps) : value};`;
};
const alignItemsValue = (value: FlexCssProps["alignItems"]) => {
@ -131,17 +138,26 @@ export const flexWrapValue = (value: FlexCssProps["wrap"]) => {
return value;
};
const cssVarValue = (value: CssVarValues) => {
const cssVarValue = (
value: CssVarValues,
extraProps?: Pick<FlexProps, "isInner">,
) => {
const isInner = Boolean(extraProps?.isInner);
if (value == null) return;
if ((value as string).includes("sizing")) {
return `var(--${value})`;
}
if ((value as string).includes("spacing")) {
if ((value as string).includes("spacing") && !isInner) {
return `var(--outer-${value})`;
}
if ((value as string).includes("spacing") && isInner) {
return `var(--inner-${value})`;
}
return value;
};

View File

@ -209,6 +209,8 @@ export interface FlexProps
style?: CSSProperties;
/** Sets the HTML [id](https://developer.mozilla.org/en-US/docs/Web/API/Element/id) for the element. */
id?: string;
/** used to specify what kind of spacing the component will use ( inner-spacing or outer-spacing) */
isInner?: boolean;
/*
* Events props

View File

@ -6,4 +6,8 @@
&[data-size="medium"] {
inline-size: var(--icon-size-2);
}
&[data-size="large"] {
inline-size: var(--icon-size-3);
}
}

View File

@ -7,6 +7,8 @@ import type { SIZES } from "../../../shared";
export type IconProps = Omit<HeadlessIconProps, "children"> & {
/** Size of the icon
* @default medium
*
* Note: we need large size for the icon only
*/
size?: keyof typeof SIZES;
/** custom icon component

View File

@ -3,6 +3,7 @@ import type {
PopoverProps,
} from "@design-system/headless";
import type { ReactNode } from "react";
import type { SIZES } from "../../../shared";
export interface ModalProps
extends Pick<
@ -13,7 +14,7 @@ export interface ModalProps
/** Size of the Modal
* @default medium
*/
size?: "small" | "medium" | "large";
size?: keyof typeof SIZES;
/** The children of the component. */
children: ReactNode;
}

View File

@ -23,7 +23,7 @@ export interface TextInputProps extends HeadlessTextInputProps {
*
* @default medium
*/
size?: keyof typeof SIZES;
size?: Omit<keyof typeof SIZES, "large">;
}
const _TextInput = (props: TextInputProps, ref: HeadlessTextInputRef) => {

View File

@ -51,14 +51,16 @@ TextInput is a component that allows users to input text.
<Story name="Size">
<Flex direction="column" gap="spacing-2">
{Object.keys(SIZES).map((size) => (
<TextInput
placeholder={size}
size={size}
startIcon={<Icon name="user" />}
key={size}
/>
))}
{Object.keys(SIZES)
.filter((size) => !["large"].includes(size))
.map((size) => (
<TextInput
placeholder={size}
size={size}
startIcon={<Icon name="user" />}
key={size}
/>
))}
</Flex>
</Story>

View File

@ -1,4 +1,5 @@
export const SIZES = {
small: "small",
medium: "medium",
large: "large",
} as const;

View File

@ -78,6 +78,7 @@ import { ZoneWidget } from "./anvil/ZoneWidget";
import { WDSHeadingWidget } from "./wds/WDSHeadingWidget";
import { WDSParagraphWidget } from "./wds/WDSParagraphWidget";
import { WDSModalWidget } from "./wds/WDSModalWidget";
import { WDSStatBoxWidget } from "./wds/WDSStatBoxWidget";
import { WDSKeyValueWidget } from "./wds/WDSKeyValueWidget";
const LegacyWidgets = [
@ -168,6 +169,7 @@ const WDSWidgets = [
WDSParagraphWidget,
WDSHeadingWidget,
WDSModalWidget,
WDSStatBoxWidget,
WDSKeyValueWidget,
];

View File

@ -0,0 +1,62 @@
import React from "react";
import type { StatBoxComponentProps } from "./types";
import styles from "./styles.module.css";
import { Flex, Icon, Text } from "@design-system/widgets";
export const StatBoxComponent = (props: StatBoxComponentProps) => {
const {
iconAlign,
iconName,
label,
sublabel,
value,
valueChange,
valueImpact,
} = props;
return (
<Flex
alignItems="center"
className={styles.statbox}
direction={iconAlign === "end" ? "row-reverse" : "row"}
gap="spacing-2"
isInner
padding="spacing-3 "
>
{iconName && iconName !== "(none)" && (
<Icon name={iconName} size="large" />
)}
<Flex direction="column" flexGrow={1} gap="spacing-3" isInner>
{label && (
<Text color="neutral" lineClamp={1} variant="footnote">
{label}
</Text>
)}
{value && (
<Flex
alignItems="end"
flexShrink={0}
gap="spacing-1"
isInner
maxWidth="calc(100% - var(--sizing-1))"
>
<Text fontWeight={500} lineClamp={1} variant="subtitle">
{value}
</Text>
{valueChange && (
<Text color={valueImpact} lineClamp={1} variant="footnote">
{valueChange}
</Text>
)}
</Flex>
)}
{sublabel && (
<Text color="neutral" lineClamp={1} variant="footnote">
{sublabel}
</Text>
)}
</Flex>
</Flex>
);
};

View File

@ -0,0 +1,6 @@
.statbox {
border: 1px solid var(--color-bd);
border-radius: var(--border-radius-1);
width: 100%;
background: var(--color-bg-elevation-2);
}

View File

@ -0,0 +1,3 @@
import type { StatBoxWidgetProps } from "../widget/types";
export interface StatBoxComponentProps extends StatBoxWidgetProps {}

View File

@ -0,0 +1,8 @@
import type { AnvilConfig } from "WidgetProvider/constants";
export const anvilConfig: AnvilConfig = {
isLargeWidget: true,
widgetSize: {
minWidth: "100%",
},
};

View File

@ -0,0 +1,7 @@
import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils";
export const autocompleteConfig = {
"!doc": "Show and highlight stats from your data sources",
"!url": "https://docs.appsmith.com/widget-reference/stat-box",
isVisible: DefaultAutocompleteDefinitions.isVisible,
};

View File

@ -0,0 +1,16 @@
import type { WidgetDefaultProps } from "WidgetProvider/constants";
import { ResponsiveBehavior } from "layoutSystems/common/utils/constants";
export const defaultsConfig = {
isVisible: true,
widgetName: "StatBoxWidget",
version: 1,
animateLoading: true,
valueImpact: "positive",
valueChange: "+120%",
value: "1500",
label: "Active Users",
sublabel: "Since 21 Jan 2022",
icon: "user",
responsiveBehavior: ResponsiveBehavior.Fill,
} as unknown as WidgetDefaultProps;

View File

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

View File

@ -0,0 +1,11 @@
import IconSVG from "../icon.svg";
import { WIDGET_TAGS } from "constants/WidgetConstants";
export const metaConfig = {
name: "Statbox",
iconSVG: IconSVG,
needsMeta: false,
isCanvas: false,
searchTags: ["statbox"],
tags: [WIDGET_TAGS.DISPLAY],
};

View File

@ -0,0 +1,141 @@
import { COLORS } from "@design-system/widgets";
import { ValidationTypes } from "constants/WidgetValidation";
import capitalize from "lodash/capitalize";
export const propertyPaneContentConfig = [
{
sectionName: "Fields",
children: [
{
propertyName: "label",
label: "Label",
helpText: "Sets the label of the statbox",
controlType: "INPUT_TEXT",
placeholderText: "Active users",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "value",
label: "Value",
helpText: "Sets the value of the statbox",
controlType: "INPUT_TEXT",
placeholderText: "257",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
],
},
{
sectionName: "Optional Fields",
children: [
{
propertyName: "iconName",
label: "Select icon",
helpText: "Sets the icon to be used for the statbox",
controlType: "ICON_SELECT_V2",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
},
},
{
propertyName: "iconAlign",
label: "Position",
helpText: "Sets the icon alignment",
controlType: "ICON_TABS",
defaultValue: "start",
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"],
},
},
},
{
propertyName: "valueChange",
label: "Value change",
helpText: "Secondary information about the value",
controlType: "INPUT_TEXT",
placeholderText: "+146%",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "valueImpact",
label: "Impact",
controlType: "DROP_DOWN",
fullWidth: true,
helpText: "Emphasizes the change's semantic impact",
options: Object.values(COLORS).map((semantic) => ({
label: capitalize(semantic),
value: semantic,
})),
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.TEXT,
params: {
allowedValues: Object.values(COLORS),
default: COLORS.accent,
},
},
},
{
propertyName: "sublabel",
label: "Sub label",
helpText: "Sets the sublabel of the statbox",
controlType: "INPUT_TEXT",
placeholderText: "Since 21 Jan 2022",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
],
},
{
sectionName: "General",
children: [
{
propertyName: "isVisible",
label: "Visible",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "animateLoading",
label: "Animate loading",
controlType: "SWITCH",
helpText: "Controls the loading of the widget",
defaultValue: true,
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
],
},
];

View File

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

View File

@ -0,0 +1 @@
export const propertyPaneStyleConfig = [];

View File

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

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 8.375C1 7.61561 1.61561 7 2.375 7H21.625C22.3844 7 23 7.61561 23 8.375V16.625C23 17.3844 22.3844 18 21.625 18H2.375C1.61561 18 1 17.3844 1 16.625V8.375ZM16.7959 10.9724C16.795 10.8822 16.83 10.7961 16.8932 10.7328C16.9563 10.6695 17.0425 10.6343 17.1326 10.635L20.0171 10.6561C20.1073 10.6567 20.1941 10.6932 20.2585 10.7574C20.3227 10.8216 20.3594 10.9083 20.3602 10.9984L20.3872 13.8829C20.3864 13.972 20.3507 14.0567 20.2877 14.1187C20.2248 14.1807 20.1395 14.2151 20.0504 14.2145C19.9613 14.2138 19.8753 14.1782 19.8112 14.1153C19.747 14.0523 19.7097 13.9671 19.7074 13.878L19.6881 11.8141L16.929 14.5788C16.8659 14.642 16.7797 14.6772 16.6896 14.6766C16.5994 14.6759 16.5126 14.6395 16.4483 14.5753C16.3839 14.511 16.3473 14.4243 16.3464 14.3342C16.3456 14.244 16.3806 14.1578 16.4438 14.0945L19.2029 11.3298L17.1389 11.3148C17.0488 11.3141 16.962 11.2776 16.8977 11.2135C16.8333 11.1493 16.7968 11.0625 16.7959 10.9724ZM4.39722 10.4079V15.25H5.29956V9.43712H4.40125L2.87048 10.5167V11.4392L4.32874 10.4079H4.39722ZM8.04096 9.80775C7.67573 10.1461 7.49311 10.5825 7.49311 11.117V11.1291H8.34711V11.117C8.34711 10.8055 8.44514 10.5543 8.64122 10.3637C8.83991 10.173 9.09909 10.0777 9.41864 10.0777C9.71943 10.0777 9.9705 10.1663 10.1719 10.3435C10.3734 10.5208 10.4741 10.7423 10.4741 11.0082C10.4741 11.223 10.4096 11.4338 10.2807 11.6406C10.1518 11.8447 9.89398 12.1482 9.50726 12.551L7.54145 14.5974V15.25H11.457V14.4403H8.79831V14.3719L10.0914 13.0626C10.5775 12.5712 10.9104 12.175 11.0904 11.8742C11.273 11.5708 11.3643 11.2646 11.3643 10.9558C11.3643 10.4778 11.183 10.0817 10.8205 9.76746C10.4579 9.45328 10.0001 9.29611 9.44683 9.29611C8.87483 9.29611 8.40623 9.46668 8.04096 9.80775Z" fill="#4C5664"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

View File

@ -0,0 +1,59 @@
import React from "react";
import type { SetterConfig } from "entities/AppTheming";
import type { WidgetState } from "widgets/BaseWidget";
import BaseWidget from "widgets/BaseWidget";
import {
metaConfig,
defaultsConfig,
autocompleteConfig,
propertyPaneContentConfig,
propertyPaneStyleConfig,
settersConfig,
anvilConfig,
} from "./../config";
import { StatBoxComponent } from "../component";
import type { StatBoxWidgetProps } from "./types";
import type { AnvilConfig } from "WidgetProvider/constants";
class WDSStatBoxWidget extends BaseWidget<StatBoxWidgetProps, WidgetState> {
constructor(props: StatBoxWidgetProps) {
super(props);
}
static type = "WDS_STATBOX_WIDGET";
static getConfig() {
return metaConfig;
}
static getDefaults() {
return defaultsConfig;
}
static getAutocompleteDefinitions() {
return autocompleteConfig;
}
static getPropertyPaneContentConfig() {
return propertyPaneContentConfig;
}
static getPropertyPaneStyleConfig() {
return propertyPaneStyleConfig;
}
static getSetterConfig(): SetterConfig {
return settersConfig;
}
static getAnvilConfig(): AnvilConfig | null {
return anvilConfig;
}
getWidgetView() {
return <StatBoxComponent {...this.props} />;
}
}
export { WDSStatBoxWidget };

View File

@ -0,0 +1,12 @@
import type { WidgetProps } from "widgets/BaseWidget";
import type { COLORS, IconProps } from "@design-system/widgets";
export interface StatBoxWidgetProps extends WidgetProps {
label?: string;
value?: string;
iconName?: IconProps["name"] | "(none)";
iconAlign?: "start" | "end";
valueChange?: string;
valueImpact?: keyof typeof COLORS;
sublabel?: string;
}

View File

@ -18,6 +18,7 @@ export const WDS_V2_WIDGET_MAP = {
RADIO_GROUP_WIDGET: "WDS_RADIO_GROUP_WIDGET",
MENU_BUTTON_WIDGET: "WDS_MENU_BUTTON_WIDGET",
MODAL_WIDGET: "WDS_MODAL_WIDGET",
STATBOX_WIDGET: "WDS_STATBOX_WIDGET",
KEY_VALUE_WIDGET: "WDS_KEY_VALUE_WIDGET",
// Anvil layout widgets