Rate widget (#4891)
* FEATURE-3357 : Rate Widget -- Create the first MVP of rate widget * FEATURE-3357 : Rate Widget -- Change the widget name into rating -- Change the widget icon -- Fix the overflow issue in case max count is big -- Fix the issue in case default rate is zero -- Add validations for maxCount and defaultRate * FEATURE-3357 : Rate Widget -- Fix an issue : Stars is cut off if maxCount is greater than 20 -- Add test cases for two validation types, RATE_DEFAULT_RATE and RATE_MAX_COUNT * FEATURE-3357 : Rate Widget -- Add expected data type for tooltip field * FEATURE-3357 : Rate Widget -- Expose maxCount * FEATURE-3357 : Rate Widget -- Change contents of isAllowHalf property -- Adjust alignment of stars dynamically -- Decrease default widget width * FEATURE-3357 : Rate Widget -- Remove a unnecessary comment block
This commit is contained in:
parent
6102f5b119
commit
38ffe86290
|
|
@ -115,6 +115,7 @@
|
|||
"react-mentions": "^4.1.1",
|
||||
"react-paginating": "^1.4.0",
|
||||
"react-player": "^2.3.1",
|
||||
"react-rating": "^2.0.5",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
|
|
|
|||
3
app/client/src/assets/icons/widget/rating.svg
Normal file
3
app/client/src/assets/icons/widget/rating.svg
Normal 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 d="M11.9545 4L14.2934 8.12197C14.4556 8.44862 14.8847 8.75905 15.2469 8.81139L20 9.75293L16.6923 13.2405C16.43 13.4949 16.2662 13.9968 16.3281 14.3559L16.8704 19.0588L12.5437 17.0908C12.2195 16.9211 11.6892 16.9211 11.3649 17.0908L7.03854 19.0572L7.58084 14.3543C7.64276 13.9952 7.47897 13.4929 7.21665 13.2388L4 9.75127L8.6621 8.80973C9.02462 8.75739 9.45373 8.44697 9.61552 8.12031L11.9545 4Z" fill="#EAEAEA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |
|
|
@ -0,0 +1,145 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Icon, Position } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
import styled from "styled-components";
|
||||
import Rating from "react-rating";
|
||||
import _ from "lodash";
|
||||
|
||||
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
|
||||
import { RateSize, RATE_SIZES } from "constants/WidgetConstants";
|
||||
import TooltipComponent from "components/ads/Tooltip";
|
||||
|
||||
/*
|
||||
Note:
|
||||
-webkit-line-clamp may seem like a wierd way to doing this
|
||||
however, it is getting more and more useful with more browser support.
|
||||
It suffices for our target browsers
|
||||
More info: https://css-tricks.com/line-clampin/
|
||||
*/
|
||||
|
||||
interface RateContainerProps {
|
||||
scrollable: boolean;
|
||||
}
|
||||
|
||||
export const RateContainer = styled.div<RateContainerProps>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: flex-start;
|
||||
overflow: auto;
|
||||
|
||||
> span {
|
||||
align-self: ${(props) => (props.scrollable ? "flex-start" : "center")};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Star = styled(Icon)`
|
||||
padding: ${(props) =>
|
||||
props.iconSize === 12 ? 2.92 : props.iconSize === 16 ? 4.37 : 4.93}px;
|
||||
`;
|
||||
|
||||
export interface RateComponentProps extends ComponentProps {
|
||||
value: number;
|
||||
isLoading: boolean;
|
||||
maxCount: number;
|
||||
size: RateSize;
|
||||
onValueChanged: (value: number) => void;
|
||||
tooltips?: Array<string>;
|
||||
activeColor?: string;
|
||||
inactiveColor?: string;
|
||||
isAllowHalf?: boolean;
|
||||
readonly?: boolean;
|
||||
leftColumn?: number;
|
||||
rightColumn?: number;
|
||||
topRow?: number;
|
||||
bottomRow?: number;
|
||||
}
|
||||
|
||||
function renderStarsWithTooltip(props: RateComponentProps) {
|
||||
const rateTooltips = props.tooltips || [];
|
||||
const rateTooltipsCount = rateTooltips.length;
|
||||
const deltaCount = props.maxCount - rateTooltipsCount;
|
||||
if (rateTooltipsCount === 0) {
|
||||
return (
|
||||
<Star
|
||||
color={props.activeColor}
|
||||
icon={IconNames.STAR}
|
||||
iconSize={RATE_SIZES[props.size]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const starWithTooltip = rateTooltips.map((tooltip) => (
|
||||
<TooltipComponent content={tooltip} key={tooltip} position={Position.TOP}>
|
||||
<Star
|
||||
color={props.activeColor}
|
||||
icon={IconNames.STAR}
|
||||
iconSize={RATE_SIZES[props.size]}
|
||||
/>
|
||||
</TooltipComponent>
|
||||
));
|
||||
const starWithoutTooltip = _.times(deltaCount, (num: number) => (
|
||||
<Star
|
||||
color={props.activeColor}
|
||||
icon={IconNames.STAR}
|
||||
iconSize={RATE_SIZES[props.size]}
|
||||
key={num}
|
||||
/>
|
||||
));
|
||||
|
||||
return _.concat(starWithTooltip, starWithoutTooltip);
|
||||
}
|
||||
|
||||
function RateComponent(props: RateComponentProps) {
|
||||
const rateContainerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
const {
|
||||
bottomRow,
|
||||
inactiveColor,
|
||||
isAllowHalf,
|
||||
leftColumn,
|
||||
maxCount,
|
||||
onValueChanged,
|
||||
readonly,
|
||||
rightColumn,
|
||||
size,
|
||||
topRow,
|
||||
value,
|
||||
} = props;
|
||||
|
||||
const [scrollable, setScrollable] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const rateContainerElement = rateContainerRef.current;
|
||||
if (
|
||||
rateContainerElement &&
|
||||
rateContainerElement.scrollHeight > rateContainerElement.clientHeight
|
||||
) {
|
||||
setScrollable(true);
|
||||
} else {
|
||||
setScrollable(false);
|
||||
}
|
||||
}, [leftColumn, rightColumn, topRow, bottomRow, maxCount, size]);
|
||||
|
||||
return (
|
||||
<RateContainer ref={rateContainerRef} scrollable={scrollable}>
|
||||
<Rating
|
||||
emptySymbol={
|
||||
<Star
|
||||
color={inactiveColor}
|
||||
icon={IconNames.STAR}
|
||||
iconSize={RATE_SIZES[size]}
|
||||
/>
|
||||
}
|
||||
fractions={isAllowHalf ? 2 : 1}
|
||||
fullSymbol={renderStarsWithTooltip(props)}
|
||||
initialRating={value}
|
||||
onChange={onValueChanged}
|
||||
readonly={readonly}
|
||||
stop={maxCount}
|
||||
/>
|
||||
</RateContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default RateComponent;
|
||||
|
|
@ -16,20 +16,13 @@ type TextStyleProps = {
|
|||
|
||||
export const BaseText = styled(Text)<TextStyleProps>``;
|
||||
|
||||
/*
|
||||
Note:
|
||||
-webkit-line-clamp may seem like a wierd way to doing this
|
||||
however, it is getting more and more useful with more browser support.
|
||||
It suffices for our target browsers
|
||||
More info: https://css-tricks.com/line-clampin/
|
||||
*/
|
||||
|
||||
export const TextContainer = styled.div`
|
||||
&& {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledText = styled(Text)<{
|
||||
scroll: boolean;
|
||||
textAlign: string;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export enum EventType {
|
|||
ON_VIDEO_END = "ON_VIDEO_END",
|
||||
ON_VIDEO_PLAY = "ON_VIDEO_PLAY",
|
||||
ON_VIDEO_PAUSE = "ON_VIDEO_PAUSE",
|
||||
ON_RATE_CHANGED = "ON_RATE_CHANGED",
|
||||
ON_IFRAME_URL_CHANGED = "ON_IFRAME_URL_CHANGED",
|
||||
ON_IFRAME_MESSAGE_RECEIVED = "ON_IFRAME_MESSAGE_RECEIVED",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ export const Colors: Record<string, string> = {
|
|||
ALTO2: "#E0DEDE",
|
||||
SEA_SHELL: "#F1F1F1",
|
||||
DANUBE: "#6A86CE",
|
||||
RATE_ACTIVE: "#FFCB45",
|
||||
RATE_INACTIVE: "#F2F2F2",
|
||||
};
|
||||
|
||||
export type Color = typeof Colors[keyof typeof Colors];
|
||||
|
|
|
|||
|
|
@ -169,6 +169,16 @@ const FIELD_VALUES: Record<
|
|||
isVisible: "boolean",
|
||||
gridGap: "number",
|
||||
},
|
||||
RATE_WIDGET: {
|
||||
maxCount: "number",
|
||||
defaultRate: "number",
|
||||
activeColor: "string",
|
||||
inactiveColor: "string",
|
||||
size: "RATE_SMALL | RATE_MEDIUM | RATE_LARGE",
|
||||
tooltips: "Array<string>",
|
||||
isVisible: "boolean",
|
||||
isDisabled: "boolean",
|
||||
},
|
||||
IFRAME_WIDGET: {
|
||||
source: "string",
|
||||
title: "string",
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ export const HelpMap = {
|
|||
path: "/widget-reference/switch",
|
||||
searchKey: "Switch",
|
||||
},
|
||||
RATE_WIDGET: {
|
||||
path: "/widget-reference/rate",
|
||||
searchKey: "Rate",
|
||||
},
|
||||
IFRAME_WIDGET: {
|
||||
path: "/widget-reference/iframe",
|
||||
searchKey: "Iframe",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export enum WidgetTypes {
|
|||
LIST_WIDGET = "LIST_WIDGET",
|
||||
SWITCH_WIDGET = "SWITCH_WIDGET",
|
||||
TABS_MIGRATOR_WIDGET = "TABS_MIGRATOR_WIDGET",
|
||||
RATE_WIDGET = "RATE_WIDGET",
|
||||
IFRAME_WIDGET = "IFRAME_WIDGET",
|
||||
}
|
||||
|
||||
|
|
@ -148,3 +149,17 @@ export const TEXT_SIZES = {
|
|||
};
|
||||
|
||||
export type TextSize = keyof typeof TextSizes;
|
||||
|
||||
export enum RateSizes {
|
||||
SMALL = "SMALL",
|
||||
MEDIUM = "MEDIUM",
|
||||
LARGE = "LARGE",
|
||||
}
|
||||
|
||||
export const RATE_SIZES = {
|
||||
SMALL: 12,
|
||||
MEDIUM: 16,
|
||||
LARGE: 21,
|
||||
};
|
||||
|
||||
export type RateSize = keyof typeof RateSizes;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export enum VALIDATION_TYPES {
|
|||
BOOLEAN = "BOOLEAN",
|
||||
OBJECT = "OBJECT",
|
||||
ARRAY = "ARRAY",
|
||||
ARRAY_OPTIONAL = "ARRAY_OPTIONAL",
|
||||
TABLE_DATA = "TABLE_DATA",
|
||||
OPTIONS_DATA = "OPTIONS_DATA",
|
||||
DATE_ISO_STRING = "DATE_ISO_STRING",
|
||||
|
|
@ -31,6 +32,8 @@ export enum VALIDATION_TYPES {
|
|||
ROW_INDICES = "ROW_INDICES",
|
||||
IMAGE = "IMAGE",
|
||||
TABS_DATA = "TABS_DATA",
|
||||
RATE_DEFAULT_RATE = "RATE_DEFAULT_RATE",
|
||||
RATE_MAX_COUNT = "RATE_MAX_COUNT",
|
||||
COLOR_PICKER_TEXT = "COLOR_PICKER_TEXT",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { ReactComponent as FormIcon } from "assets/icons/widget/form.svg";
|
|||
import { ReactComponent as MapIcon } from "assets/icons/widget/map.svg";
|
||||
import { ReactComponent as ModalIcon } from "assets/icons/widget/modal.svg";
|
||||
import { ReactComponent as ListIcon } from "assets/icons/widget/list.svg";
|
||||
import { ReactComponent as RatingIcon } from "assets/icons/widget/rating.svg";
|
||||
import { ReactComponent as EmbedIcon } from "assets/icons/widget/embed.svg";
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
|
|
@ -143,6 +144,11 @@ export const WidgetIcons: {
|
|||
<ListIcon />
|
||||
</IconWrapper>
|
||||
),
|
||||
RATE_WIDGET: (props: IconProps) => (
|
||||
<IconWrapper {...props}>
|
||||
<RatingIcon />
|
||||
</IconWrapper>
|
||||
),
|
||||
IFRAME_WIDGET: (props: IconProps) => (
|
||||
<IconWrapper {...props}>
|
||||
<EmbedIcon />
|
||||
|
|
|
|||
|
|
@ -1091,6 +1091,19 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
|
|||
],
|
||||
},
|
||||
},
|
||||
RATE_WIDGET: {
|
||||
rows: 1 * GRID_DENSITY_MIGRATION_V1,
|
||||
columns: 2.5 * GRID_DENSITY_MIGRATION_V1,
|
||||
maxCount: 5,
|
||||
defaultRate: 5,
|
||||
activeColor: Colors.RATE_ACTIVE,
|
||||
inactiveColor: Colors.RATE_INACTIVE,
|
||||
size: "MEDIUM",
|
||||
isRequired: false,
|
||||
isAllowHalf: false,
|
||||
isDisabled: false,
|
||||
widgetName: "Rating",
|
||||
},
|
||||
[WidgetTypes.IFRAME_WIDGET]: {
|
||||
source: "https://www.wikipedia.org/",
|
||||
borderOpacity: 100,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,11 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
|
|||
widgetCardName: "Modal",
|
||||
key: generateReactKey(),
|
||||
},
|
||||
{
|
||||
type: "RATE_WIDGET",
|
||||
widgetCardName: "Rating",
|
||||
key: generateReactKey(),
|
||||
},
|
||||
{
|
||||
type: "IFRAME_WIDGET",
|
||||
widgetCardName: "Iframe",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { VideoWidgetProps } from "widgets/VideoWidget";
|
|||
import { SkeletonWidgetProps } from "../../widgets/SkeletonWidget";
|
||||
import { SwitchWidgetProps } from "widgets/SwitchWidget";
|
||||
import { ListWidgetProps } from "../../widgets/ListWidget/ListWidget";
|
||||
import { RateWidgetProps } from "../../widgets/RateWidget";
|
||||
import { IframeWidgetProps } from "widgets/IframeWidget";
|
||||
|
||||
const initialState: WidgetConfigReducerState = WidgetConfigResponse;
|
||||
|
|
@ -83,6 +84,7 @@ export interface WidgetConfigReducerState {
|
|||
ICON_WIDGET: Partial<IconWidgetProps> & WidgetConfigProps;
|
||||
SKELETON_WIDGET: Partial<SkeletonWidgetProps> & WidgetConfigProps;
|
||||
LIST_WIDGET: Partial<ListWidgetProps<WidgetProps>> & WidgetConfigProps;
|
||||
RATE_WIDGET: Partial<RateWidgetProps> & WidgetConfigProps;
|
||||
IFRAME_WIDGET: Partial<IframeWidgetProps> & WidgetConfigProps;
|
||||
};
|
||||
configVersion: number;
|
||||
|
|
|
|||
|
|
@ -103,6 +103,10 @@ import SwitchWidget, {
|
|||
import TabsMigratorWidget, {
|
||||
ProfiledTabsMigratorWidget,
|
||||
} from "widgets/Tabs/TabsMigrator";
|
||||
import RateWidget, {
|
||||
RateWidgetProps,
|
||||
ProfiledRateWidget,
|
||||
} from "widgets/RateWidget";
|
||||
import IframeWidget, {
|
||||
IframeWidgetProps,
|
||||
ProfiledIframeWidget,
|
||||
|
|
@ -460,6 +464,19 @@ export default class WidgetBuilderRegistry {
|
|||
ModalWidget.getPropertyPaneConfig(),
|
||||
);
|
||||
|
||||
WidgetFactory.registerWidgetBuilder(
|
||||
"RATE_WIDGET",
|
||||
{
|
||||
buildWidget(widgetData: RateWidgetProps): JSX.Element {
|
||||
return <ProfiledRateWidget {...widgetData} />;
|
||||
},
|
||||
},
|
||||
RateWidget.getDerivedPropertiesMap(),
|
||||
RateWidget.getDefaultPropertiesMap(),
|
||||
RateWidget.getMetaPropertiesMap(),
|
||||
RateWidget.getPropertyPaneConfig(),
|
||||
);
|
||||
|
||||
WidgetFactory.registerWidgetBuilder(
|
||||
WidgetTypes.IFRAME_WIDGET,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -246,6 +246,13 @@ export const entityDefinitions = {
|
|||
items: generateTypeDef(widget.items),
|
||||
listData: generateTypeDef(widget.listData),
|
||||
}),
|
||||
RATE_WIDGET: {
|
||||
"!doc": "Rating widget is used to display ratings in your app.",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/rate",
|
||||
isVisible: isVisible,
|
||||
value: "number",
|
||||
maxCount: "number",
|
||||
},
|
||||
IFRAME_WIDGET: {
|
||||
"!doc": "Iframe widget is used to display iframes in your app.",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/iframe",
|
||||
|
|
|
|||
191
app/client/src/widgets/RateWidget/index.tsx
Normal file
191
app/client/src/widgets/RateWidget/index.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import React from "react";
|
||||
import BaseWidget, { WidgetProps, WidgetState } from "../BaseWidget";
|
||||
import { WidgetType, RateSize } from "constants/WidgetConstants";
|
||||
import RateComponent from "components/designSystems/blueprint/RateComponent";
|
||||
import { VALIDATION_TYPES } from "constants/WidgetValidation";
|
||||
import { DerivedPropertiesMap } from "utils/WidgetFactory";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import withMeta, { WithMeta } from "widgets/MetaHOC";
|
||||
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
|
||||
class RateWidget extends BaseWidget<RateWidgetProps, WidgetState> {
|
||||
static getPropertyPaneConfig() {
|
||||
return [
|
||||
{
|
||||
sectionName: "General",
|
||||
children: [
|
||||
{
|
||||
propertyName: "maxCount",
|
||||
helpText: "Sets the maximum limit of the number of stars",
|
||||
label: "Max count",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "Enter max count",
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: VALIDATION_TYPES.RATE_MAX_COUNT,
|
||||
},
|
||||
{
|
||||
propertyName: "defaultRate",
|
||||
helpText: "Sets the default number of stars",
|
||||
label: "Default rate",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "Enter default value",
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: VALIDATION_TYPES.RATE_DEFAULT_RATE,
|
||||
},
|
||||
{
|
||||
propertyName: "activeColor",
|
||||
label: "Active color",
|
||||
controlType: "COLOR_PICKER",
|
||||
isBindProperty: false,
|
||||
isTriggerProperty: false,
|
||||
},
|
||||
{
|
||||
propertyName: "inactiveColor",
|
||||
label: "Inactive color",
|
||||
controlType: "COLOR_PICKER",
|
||||
isBindProperty: false,
|
||||
isTriggerProperty: false,
|
||||
},
|
||||
{
|
||||
propertyName: "tooltips",
|
||||
helpText: "Sets the tooltip contents of stars",
|
||||
label: "Tooltips",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "Enter tooltips array",
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: VALIDATION_TYPES.ARRAY_OPTIONAL,
|
||||
},
|
||||
{
|
||||
propertyName: "size",
|
||||
label: "Size",
|
||||
controlType: "DROP_DOWN",
|
||||
options: [
|
||||
{
|
||||
label: "Small",
|
||||
value: "SMALL",
|
||||
},
|
||||
{
|
||||
label: "Medium",
|
||||
value: "MEDIUM",
|
||||
},
|
||||
{
|
||||
label: "Large",
|
||||
value: "LARGE",
|
||||
},
|
||||
],
|
||||
isBindProperty: false,
|
||||
isTriggerProperty: false,
|
||||
},
|
||||
{
|
||||
propertyName: "isAllowHalf",
|
||||
helpText: "Controls if user can submit half stars",
|
||||
label: "Allow half stars",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: VALIDATION_TYPES.BOOLEAN,
|
||||
},
|
||||
{
|
||||
propertyName: "isVisible",
|
||||
helpText: "Controls the visibility of the widget",
|
||||
label: "Visible",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: VALIDATION_TYPES.BOOLEAN,
|
||||
},
|
||||
{
|
||||
propertyName: "isDisabled",
|
||||
helpText: "Disables input to the widget",
|
||||
label: "Disabled",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: VALIDATION_TYPES.BOOLEAN,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionName: "Actions",
|
||||
children: [
|
||||
{
|
||||
helpText: "Triggers an action when the rate is changed",
|
||||
propertyName: "onRateChanged",
|
||||
label: "onChange",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
static getDefaultPropertiesMap(): Record<string, string> {
|
||||
return {
|
||||
rate: "defaultRate",
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedPropertiesMap(): DerivedPropertiesMap {
|
||||
return {
|
||||
value: `{{ this.rate }}`,
|
||||
};
|
||||
}
|
||||
|
||||
static getMetaPropertiesMap(): Record<string, any> {
|
||||
return {
|
||||
rate: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
valueChangedHandler = (value: number) => {
|
||||
this.props.updateWidgetMetaProperty("rate", value, {
|
||||
triggerPropertyName: "onRateChanged",
|
||||
dynamicString: this.props.onRateChanged,
|
||||
event: {
|
||||
type: EventType.ON_RATE_CHANGED,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
getPageView() {
|
||||
return (
|
||||
(this.props.rate || this.props.rate === 0) && (
|
||||
<RateComponent
|
||||
key={this.props.widgetId}
|
||||
onValueChanged={this.valueChangedHandler}
|
||||
readonly={this.props.isDisabled}
|
||||
value={this.props.rate}
|
||||
{...this.props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getWidgetType(): WidgetType {
|
||||
return "RATE_WIDGET";
|
||||
}
|
||||
}
|
||||
|
||||
export interface RateWidgetProps extends WidgetProps, WithMeta {
|
||||
maxCount: number;
|
||||
size: RateSize;
|
||||
defaultRate?: number;
|
||||
rate?: number;
|
||||
activeColor?: string;
|
||||
inactiveColor?: string;
|
||||
isAllowHalf?: boolean;
|
||||
onRateChanged?: string;
|
||||
tooltips?: Array<string>;
|
||||
}
|
||||
|
||||
export default RateWidget;
|
||||
export const ProfiledRateWidget = Sentry.withProfiler(withMeta(RateWidget));
|
||||
|
|
@ -509,6 +509,190 @@ describe("List data validator", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Rating widget : defaultRate", () => {
|
||||
const validator = VALIDATORS.RATE_DEFAULT_RATE;
|
||||
it("An input is not a number", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: undefined,
|
||||
output: {
|
||||
isValid: false,
|
||||
parsed: 0,
|
||||
message: 'This value does not evaluate to type "number"',
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "string",
|
||||
output: {
|
||||
isValid: false,
|
||||
parsed: 0,
|
||||
message: 'This value does not evaluate to type "number"',
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const testCase of cases) {
|
||||
const response = validator(testCase.input, DUMMY_WIDGET, {});
|
||||
expect(response).toStrictEqual(testCase.output);
|
||||
}
|
||||
});
|
||||
|
||||
it("An input is a number & maxCount", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 3,
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 5,
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 6,
|
||||
output: {
|
||||
isValid: false,
|
||||
parsed: 6,
|
||||
message: "This value must be less than or equal to max count",
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const testCase of cases) {
|
||||
const response = validator(
|
||||
testCase.input,
|
||||
{ ...DUMMY_WIDGET, maxCount: 5 },
|
||||
{},
|
||||
);
|
||||
expect(response).toStrictEqual(testCase.output);
|
||||
}
|
||||
});
|
||||
|
||||
it("An input is a number & isAllowedHalf=true", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 3,
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 3.5,
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: 3.5,
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const testCase of cases) {
|
||||
const response = validator(
|
||||
testCase.input,
|
||||
{ ...DUMMY_WIDGET, isAllowHalf: true },
|
||||
{},
|
||||
);
|
||||
expect(response).toStrictEqual(testCase.output);
|
||||
}
|
||||
});
|
||||
|
||||
it("An input is a number & isAllowedHalf=false", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 3,
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 3.5,
|
||||
output: {
|
||||
isValid: false,
|
||||
parsed: 3.5,
|
||||
message: `This value can be a decimal onlf if 'Allow half' is true`,
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const testCase of cases) {
|
||||
const response = validator(
|
||||
testCase.input,
|
||||
{ ...DUMMY_WIDGET, isAllowHalf: false },
|
||||
{},
|
||||
);
|
||||
expect(response).toStrictEqual(testCase.output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rating widget : maxCount", () => {
|
||||
const validator = VALIDATORS.RATE_MAX_COUNT;
|
||||
it("An input is not a number", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: undefined,
|
||||
output: {
|
||||
isValid: false,
|
||||
parsed: 0,
|
||||
message: 'This value does not evaluate to type "number"',
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "string",
|
||||
output: {
|
||||
isValid: false,
|
||||
parsed: 0,
|
||||
message: 'This value does not evaluate to type "number"',
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const testCase of cases) {
|
||||
const response = validator(testCase.input, DUMMY_WIDGET, {});
|
||||
expect(response).toStrictEqual(testCase.output);
|
||||
}
|
||||
});
|
||||
|
||||
it("An input is a number, should be an integer", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: 3,
|
||||
output: {
|
||||
isValid: true,
|
||||
parsed: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 3.5,
|
||||
output: {
|
||||
isValid: false,
|
||||
parsed: 3.5,
|
||||
message: "This value must be integer",
|
||||
},
|
||||
},
|
||||
];
|
||||
for (const testCase of cases) {
|
||||
const response = validator(testCase.input, DUMMY_WIDGET, {});
|
||||
expect(response).toStrictEqual(testCase.output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Color Picker Text validator", () => {
|
||||
const validator = VALIDATORS.COLOR_PICKER_TEXT;
|
||||
const inputs = [
|
||||
|
|
|
|||
|
|
@ -227,6 +227,39 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
|||
};
|
||||
}
|
||||
},
|
||||
[VALIDATION_TYPES.ARRAY_OPTIONAL]: (value: any): ValidationResponse => {
|
||||
let parsed = value;
|
||||
try {
|
||||
if (!value) {
|
||||
return {
|
||||
isValid: true,
|
||||
parsed: undefined,
|
||||
transformed: undefined,
|
||||
};
|
||||
}
|
||||
if (isString(value)) {
|
||||
parsed = JSON.parse(parsed as string);
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
return {
|
||||
isValid: false,
|
||||
parsed: [],
|
||||
transformed: parsed,
|
||||
message: `${WIDGET_TYPE_VALIDATION_ERROR} "Array"`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true, parsed, transformed: parsed };
|
||||
} catch (e) {
|
||||
return {
|
||||
isValid: false,
|
||||
parsed: [],
|
||||
transformed: parsed,
|
||||
message: `${WIDGET_TYPE_VALIDATION_ERROR} "Array"`,
|
||||
};
|
||||
}
|
||||
},
|
||||
[VALIDATION_TYPES.TABS_DATA]: (
|
||||
value: any,
|
||||
props: WidgetProps,
|
||||
|
|
@ -1034,6 +1067,56 @@ export const VALIDATORS: Record<VALIDATION_TYPES, Validator> = {
|
|||
message: `${WIDGET_TYPE_VALIDATION_ERROR}: number[]`,
|
||||
};
|
||||
},
|
||||
[VALIDATION_TYPES.RATE_DEFAULT_RATE]: (
|
||||
value: any,
|
||||
props: WidgetProps,
|
||||
): ValidationResponse => {
|
||||
const { isValid, message, parsed } = VALIDATORS[VALIDATION_TYPES.NUMBER](
|
||||
value,
|
||||
props,
|
||||
);
|
||||
if (!isValid) {
|
||||
return { isValid, parsed, message };
|
||||
}
|
||||
// default rate must be less than max count
|
||||
if (!isNaN(props.maxCount) && Number(value) > Number(props.maxCount)) {
|
||||
return {
|
||||
isValid: false,
|
||||
parsed,
|
||||
message: `This value must be less than or equal to max count`,
|
||||
};
|
||||
}
|
||||
// default rate can be a decimal onlf if Allow half property is true
|
||||
if (!props.isAllowHalf && !Number.isInteger(parsed)) {
|
||||
return {
|
||||
isValid: false,
|
||||
parsed,
|
||||
message: `This value can be a decimal onlf if 'Allow half' is true`,
|
||||
};
|
||||
}
|
||||
return { isValid, parsed };
|
||||
},
|
||||
[VALIDATION_TYPES.RATE_MAX_COUNT]: (
|
||||
value: any,
|
||||
props: WidgetProps,
|
||||
): ValidationResponse => {
|
||||
const { isValid, message, parsed } = VALIDATORS[VALIDATION_TYPES.NUMBER](
|
||||
value,
|
||||
props,
|
||||
);
|
||||
if (!isValid) {
|
||||
return { isValid, parsed, message };
|
||||
}
|
||||
// max count must be integer
|
||||
if (!Number.isInteger(parsed)) {
|
||||
return {
|
||||
isValid: false,
|
||||
parsed,
|
||||
message: `This value must be integer`,
|
||||
};
|
||||
}
|
||||
return { isValid, parsed };
|
||||
},
|
||||
[VALIDATION_TYPES.COLOR_PICKER_TEXT]: (
|
||||
value: any,
|
||||
props: WidgetProps,
|
||||
|
|
|
|||
|
|
@ -3713,6 +3713,11 @@
|
|||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
|
||||
"@types/lodash@^4.14.105":
|
||||
version "4.14.170"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
|
||||
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
|
||||
|
||||
"@types/lodash@^4.14.120":
|
||||
version "4.14.162"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.162.tgz#65d78c397e0d883f44afbf1f7ba9867022411470"
|
||||
|
|
@ -3932,6 +3937,15 @@
|
|||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^16.0.40":
|
||||
version "16.14.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.8.tgz#4aee3ab004cb98451917c9b7ada3c7d7e52db3fe"
|
||||
integrity sha512-QN0/Qhmx+l4moe7WJuTxNiTsjBwlBGHqKGvInSQCBdo7Qio0VtOqwsC0Wq7q3PbJlB0cR4Y4CVo1OOe6BOsOmA==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/reactcss@*":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834"
|
||||
|
|
@ -3961,6 +3975,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
|
||||
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
|
||||
|
||||
"@types/set-cookie-parser@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.0.tgz#10cc0446bad372827671a5195fbd14ebce4a9baf"
|
||||
|
|
@ -14599,6 +14618,14 @@ react-popper@^2.2.4, react-popper@^2.2.5:
|
|||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-rating@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-rating/-/react-rating-2.0.5.tgz#2c9d7ebe5907db0361ba28b7c19f0394e6e4dd76"
|
||||
integrity sha512-uldxgLCe5bzqGX7V+7/bPgQQj2Kok6eiMgTMxjKOhfhnQkFLDlc4TjMlp7gaJFAHWdbiOnqpiShI7z8as6oWtg==
|
||||
dependencies:
|
||||
"@types/lodash" "^4.14.105"
|
||||
"@types/react" "^16.0.40"
|
||||
|
||||
react-redux@^7.1.1, react-redux@^7.1.3:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user