import React, { useEffect, useRef } from "react"; import FormControl from "pages/Editor/FormControl"; import { Icon, IconSize } from "design-system-old"; import styled from "styled-components"; import { FieldArray, getFormValues } from "redux-form"; import { ControlProps } from "./BaseControl"; import _ from "lodash"; import { useSelector } from "react-redux"; import { getBindingOrConfigPathsForWhereClauseControl } from "entities/Action/actionProperties"; import { WhereClauseSubComponent } from "./utils"; import { TooltipComponent as Tooltip } from "design-system-old"; import useResponsiveBreakpoints from "utils/hooks/useResponsiveBreakpoints"; import { Colors } from "constants/Colors"; //Dropdwidth and Icon have fixed widths const DropdownWidth = 82; //pixel value const OperatorDropdownWidth = 100; // operators should have longer dropdown widths. const Margin = 8; // Type of the value for each condition export type whereClauseValueType = { condition?: string; children?: [whereClauseValueType]; key?: string; value?: string; }; // Form config for the value field const valueFieldConfig: any = { key: "value", controlType: "QUERY_DYNAMIC_INPUT_TEXT", placeholderText: "Value", }; // Form config for the key field const keyFieldConfig: any = { key: "key", controlType: "QUERY_DYNAMIC_INPUT_TEXT", placeholderText: "Column name", }; // Form config for the condition field const conditionFieldConfig: any = { key: "operator", controlType: "DROP_DOWN", initialValue: "EQ", options: [], }; // Form config for the operator field const logicalFieldConfig: any = { key: "condition", controlType: "DROP_DOWN", initialValue: "EQ", }; const LogicalFieldValue: any = styled.p<{ width: string | undefined; size: string; }>` ${(props) => (props.width ? "width: " + props.width + ";" : "")} margin: 4px 0px; border: solid 1.2px transparent; text-align: right; color: var(--appsmith-color-black-400); font-size: 14px; flex-shrink: 0; ${(props) => props.size === "small" && ` margin: 4px 0 0; text-align: left; `} `; // Component for the delete Icon const CenteredIcon = styled(Icon)<{ alignSelf?: string; top?: string; }>` position: relative; align-self: ${(props) => (props.alignSelf ? props.alignSelf : "center")}; top: ${(props) => (props.top ? props.top : "0px")}; &.hide { opacity: 0; pointer-events: none; } `; // We are setting a background color for the last two nested levels const handleSecondaryBoxBackgroudColor = ( currentNestingLevel: number, nestedLevels: number, ) => { if (currentNestingLevel === nestedLevels) { return `background-color: ${Colors.GRAY_100};`; } else if (currentNestingLevel === nestedLevels - 1) { return `background-color: ${Colors.GRAY_50};`; } else { return ""; } }; // Wrapper inside the main box, contains the dropdown and ConditionWrapper const SecondaryBox = styled.div<{ currentNestingLevel: number; nestedLevels: number; showBorder: boolean; size: string; }>` display: flex; flex-direction: column; position: relative; border: solid 1.2px #e0dede; border-width: ${(props) => (props?.showBorder ? "1.2px" : "0px")}; padding: ${(props) => props?.showBorder ? "0px 12px 28px 8px" : "4px 12px 24px 0px"}; width: 100%; // Setting a max width to not have it really elongate on very large screens max-width: 2000px; ${(props) => props.size === "small" && ` ${handleSecondaryBoxBackgroudColor( props.currentNestingLevel, props.nestedLevels, )} padding-bottom: 20px; `} `; // Wrapper to contain either a ConditionComponent or ConditionBlock const ConditionWrapper = styled.div<{ size: string }>` display: flex; flex-direction: row; align-items: start; width: 100%; gap: 8px; margin-top: 12px; ${(props) => props.size === "small" && ` margin-top: 0px; gap: 0px; flex-direction: column; align-items: start; `} `; // Wrapper to contain a single condition statement const ConditionBox = styled.div<{ size?: string }>` display: grid; // The 4 elements(3 input fields and a close button) are horizontally aligned // by default grid-template-columns: auto 100px auto max-content; grid-column-gap: 12px; grid-row-gap: 8px; width: 100%; ${(props) => props.size === "small" && ` // Smallest width of the component such that the text CTA's "ADD GROUP CONDITION" // fits in the available space without overflow min-width: 325px; margin: 8px 0px; // In small space we shift to a two column layout where the three inputs // are verticall aligned one below the other. grid-template-columns: repeat(2, max-content); grid-template-rows: repeat(3, max-content); grid-column-gap: 8px; // The three input fields will be in the first column & :not(:nth-child(4)) { grid-column-start: 1; } // The fourth element i.e the close button will be placed in the second row // to have it center aligned & :nth-child(4) { grid-column-start: 2; grid-row-start: 2; } `} `; // Box containing the action buttons to add more filters const ActionBox = styled.div<{ marginLeft: string; size: string }>` display: flex; margin-top: 16px; flex-direction: row; gap: 20px; width: max-content; justify-content: space-between; position: absolute; height: 24px; text-transform: uppercase; background-color: inherit; bottom: 0px; margin-left: ${(props) => props.marginLeft}; ${(props) => props.size === "small" && ` margin-left: 0; `} `; // The final button to add more filters/ filter groups const AddMoreAction = styled.div<{ isDisabled?: boolean; size?: string }>` cursor: pointer; display: flex; align-items: center; font-size: 12px; font-weight: 500; line-height: 14px; letter-spacing: 0.6px; color: ${(props) => props.isDisabled ? "var(--appsmith-color-black-300)" : "#858282;"}; // Hide the "ADD GROUP CONDITION" text when in small space and is disabled ${(props) => props.size === "small" && props.isDisabled && ` display: none; `} `; const GroupConditionBox = styled.div<{ size: string }>` display: flex; flex-direction: row; gap: 12px; width: 100%; ${(props) => props.size === "small" && ` gap: 8px; margin: 8px 0px; flex-direction: row; min-width: max-content; `} `; const StyledTooltip = styled(Tooltip)` display: flex; align-items: center; .bp3-tooltip.ads-global-tooltip .bp3-popover-content { padding: 8px 12px; line-height: 16px; text-transform: none; } .bp3-tooltip.ads-global-tooltip .bp3-popover-arrow[style*="left"] { left: auto !important; right: 0px; } `; // Component to display single line of condition, includes 2 inputs and 1 dropdown function ConditionComponent(props: any, index: number) { // Custom styles have to be passed as props, otherwise the UI will be disproportional const keyPath = getBindingOrConfigPathsForWhereClauseControl( props.field, WhereClauseSubComponent.Key, ); const valuePath = getBindingOrConfigPathsForWhereClauseControl( props.field, WhereClauseSubComponent.Value, ); const conditionPath = getBindingOrConfigPathsForWhereClauseControl( props.field, WhereClauseSubComponent.Condition, ); return ( {/* Component to input the LHS for single condition */} {/* Component to select the operator for the 2 inputs */} {/* Component to input the RHS for single component */} {/* Component to render the delete icon */} { e.stopPropagation(); props.onDeletePressed(index); }} size={IconSize.SMALL} top="-1px" /> ); } // This is the block which contains an operator and multiple conditions/ condition blocks function ConditionBlock(props: any) { const targetRef = useRef(null); // Smallest width of the component below which the individual input fields don't // decrease in width anymore so we decide to shift to small space layout at this point const size = useResponsiveBreakpoints(targetRef, [{ small: 505 }]); const formValues: any = useSelector((state) => getFormValues(props.formName)(state), ); const onDeletePressed = (index: number) => { props.fields.remove(index); }; // sometimes, this condition runs before the appropriate formValues has been initialized with the correct query values. useEffect(() => { // so make sure the new formValue has been initialized with the where object, // especially when switching between various queries across the same Query editor form. const whereConfigValue = _.get(formValues, props.configProperty); // if the where object exists then it means the initialization of the form has been completed. // if the where object exists and the length of children field is less than one, add a new field. if (props.fields.length < 1 && !!whereConfigValue) { if (props.currentNestingLevel === 0) { props.fields.push({ condition: props.comparisonTypes[0].value, }); } else { props.onDeletePressed(props.index); } } }, [props.fields.length]); let isDisabled = false; if (props.logicalTypes.length === 1) { isDisabled = true; } const logicalFieldPath = getBindingOrConfigPathsForWhereClauseControl( props.configProperty, WhereClauseSubComponent.Condition, ); const logicalFieldValue = _.get(formValues, logicalFieldPath); return ( = 1} size={size} > {props.fields && props.fields.length > 0 && props.fields.map((field: any, index: number) => { const fieldValue: whereClauseValueType = props.fields.get(index); return ( {/* Component to render the joining operator between multiple conditions */} {index == 0 ? ( Where ) : index == 1 ? ( ) : ( {logicalFieldValue} )} {!!fieldValue && "children" in fieldValue ? ( { e.stopPropagation(); onDeletePressed(index); }} size={IconSize.SMALL} top={"24px"} /> ) : ( ConditionComponent( { onDeletePressed, field, formName: props.formName, comparisonTypes: props.comparisonTypes, maxWidth: props.maxWidth, currentNestingLevel: props.currentNestingLevel, size, }, index, ) )} ); })} props.fields.push({ key: "", condition: props.comparisonTypes[0].value, value: "", }) // Add empty and key and value as it will required to create binding paths in getBindingPathsOfAction() at ActionProperties.ts } > Add Condition {/* Check if the config allows more nesting, if it does, allow for adding more blocks */} For S3 only 4 nested where
condition group is allowed. } disabled={props.currentNestingLevel < props.nestedLevels} donotUsePortal position="bottom" > { if (props.currentNestingLevel < props.nestedLevels) { props.fields.push({ condition: props.logicalTypes[0].value, children: [ { condition: props.comparisonTypes[0].value, }, ], }); } }} size={size} > Add Group Condition
); } export default function WhereClauseControl(props: WhereClauseControlProps) { const { comparisonTypes, // All possible keys for the comparison configProperty, // JSON path for the where clause data formName, // Name of the form, used by redux-form lib to store the data in redux store logicalTypes, // All possible keys for the logical operators joining multiple conditions nestedLevels, // Number of nested levels allowed } = props; // Max width is designed in a way that the proportion stays same even after nesting const maxWidth = 60; //in vw return ( ); } export type WhereClauseControlProps = ControlProps;