fix: Column tile reposition on focus state of Input element inside property pane configuration for Table and Tabs widget (#10046)

This commit is contained in:
Vicky Bansal 2022-02-04 16:29:54 +05:30 committed by GitHub
parent 324b077ffa
commit b7ebc7501e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 500 additions and 434 deletions

View File

@ -1,11 +1,11 @@
{
"tabWidget": ".t--draggable-tabswidget",
"tabInput": ".t--draggable-tabswidget span.t--widget-name",
"tabName": ".t--property-control-tabs input",
"tabDefault": ".t--property-control-defaulttab .CodeMirror-code",
"tabButton": ".t--property-control-tabs button",
"tabDelete": ".t--property-control-tabs .t--delete-tab-btn",
"tabContainer": "div[type='TABS_WIDGET']",
"tabEdit": ".t--property-control-tabs .t--edit-column-btn",
"tabVisibility": ".t--property-control-visible .bp3-control-indicator"
}
"tabWidget": ".t--draggable-tabswidget",
"tabInput": ".t--draggable-tabswidget span.t--widget-name",
"tabName": ".t--property-control-tabs input",
"tabDefault": ".t--property-control-defaulttab .CodeMirror-code",
"tabButton": ".t--property-control-tabs button",
"tabDelete": ".t--property-control-tabs .t--delete-column-btn",
"tabContainer": "div[type='TABS_WIDGET']",
"tabEdit": ".t--property-control-tabs .t--edit-column-btn",
"tabVisibility": ".t--property-control-visible .bp3-control-indicator"
}

View File

@ -60,10 +60,27 @@ const DraggableListWrapper = styled.div`
`;
function DraggableList(props: any) {
const { itemHeight, ItemRenderer, items, onUpdate } = props;
const {
fixedHeight,
focusedIndex,
itemHeight,
ItemRenderer,
items,
onUpdate,
updateDragging,
} = props;
const listContainerHeight =
fixedHeight && fixedHeight < items.length * itemHeight
? fixedHeight
: items.length * itemHeight;
const shouldReRender = get(props, "shouldReRender", true);
// order of items in the list
const order = useRef<any>(items.map((_: any, index: any) => index));
const displacement = useRef<number>(0);
const dragging = useRef<boolean>(false);
const listRef = useRef<HTMLDivElement | null>(null);
const onDrop = (originalIndex: number, newIndex: number) => {
onUpdate(order.current, originalIndex, newIndex);
@ -82,6 +99,29 @@ function DraggableList(props: any) {
}
}, [items]);
useEffect(() => {
if (focusedIndex && listRef && listRef.current) {
const container = listRef.current;
if (focusedIndex * itemHeight < container.scrollTop) {
listRef.current.scrollTo({
top: (focusedIndex - 1) * itemHeight,
left: 0,
behavior: "smooth",
});
} else if (
(focusedIndex + 1) * itemHeight >
listRef.current.scrollTop + listRef.current.clientHeight
) {
listRef.current.scrollTo({
top: (focusedIndex + 1) * itemHeight - listRef.current.clientHeight,
left: 0,
behavior: "smooth",
});
}
}
}, [focusedIndex]);
const [springs, setSprings] = useSprings<any>(
items.length,
updateSpringStyles(order.current, itemHeight),
@ -90,60 +130,122 @@ function DraggableList(props: any) {
const bind: any = useDrag<any>((props: any) => {
const originalIndex = props.args[0];
const curIndex = order.current.indexOf(originalIndex);
const curRow = clamp(
Math.round((curIndex * itemHeight + props.movement[1]) / itemHeight),
0,
items.length - 1,
);
const newOrder = swap(order.current, curIndex, curRow);
setSprings(
dragIdleSpringStyles(newOrder, {
down: props.down,
originalIndex,
curIndex,
y: props.movement[1],
itemHeight,
}),
);
if (curRow !== curIndex) {
// Feed springs new style data, they'll animate the view without causing a single render
if (!props.down) {
order.current = newOrder;
setSprings(updateSpringStyles(order.current, itemHeight));
debounce(onDrop, 400)(curIndex, curRow);
const pointerFromTop = props.xy[1];
if (listRef && listRef.current) {
const containerCoordinates = listRef?.current.getBoundingClientRect();
const container = listRef.current;
if (containerCoordinates) {
const containerDistanceFromTop = containerCoordinates.top;
if (props.dragging) {
if (pointerFromTop < containerDistanceFromTop + itemHeight / 2) {
// Scroll inside container till first element in list is completely visible
if (container.scrollTop > 0) {
container.scrollTop -= itemHeight / 10;
}
} else if (
pointerFromTop >=
containerDistanceFromTop + container.clientHeight - itemHeight / 2
) {
// Scroll inside container till container cannnot be scrolled more towards bottom
if (
container.scrollTop <=
springs.length * itemHeight -
container.clientHeight -
itemHeight / 2
) {
container.scrollTop += itemHeight / 10;
}
}
// finding distance of current pointer from the top of the container to find the final position
// currIndex * itemHeight for the initial position
// subtraction formar with latter for displacement
displacement.current =
pointerFromTop -
containerDistanceFromTop +
container.scrollTop -
curIndex * itemHeight -
itemHeight / 2;
if (!dragging.current && Math.abs(displacement.current) > 10) {
dragging.current = props.dragging;
updateDragging(dragging.current);
}
} else {
if (dragging.current) {
dragging.current = props.dragging;
updateDragging(dragging.current);
}
}
const curRow = clamp(
Math.round(
(curIndex * itemHeight + displacement.current) / itemHeight,
),
0,
items.length - 1,
);
const newOrder = swap(order.current, curIndex, curRow);
setSprings(
dragIdleSpringStyles(newOrder, {
down: props.down,
originalIndex,
curIndex,
y: Math.abs(displacement.current) > 10 ? displacement.current : 0,
itemHeight,
}),
);
if (curRow !== curIndex) {
// Feed springs new style data, they'll animate the view without causing a single render
if (!props.down) {
order.current = newOrder;
setSprings(updateSpringStyles(order.current, itemHeight));
debounce(onDrop, 400)(curIndex, curRow);
}
}
}
}
});
return (
<DraggableListWrapper
className="content"
onMouseDown={() => {
// set events to null to stop other parent draggable elements execution(ex: Property pane)
document.onmouseup = null;
document.onmousemove = null;
<div
ref={listRef}
style={{
height: listContainerHeight,
overflowY: "auto",
zIndex: 1,
}}
style={{ height: items.length * itemHeight }}
>
{springs.map(({ scale, y, zIndex }, i) => (
<animated.div
{...bind(i)}
data-rbd-draggable-id={items[i].id}
key={i}
style={{
zIndex,
width: "100%",
transform: to(
[y, scale],
(y, s) => `translate3d(0,${y}px,0) scale(${s})`,
),
}}
>
<div>
<ItemRenderer index={i} item={items[i]} />
</div>
</animated.div>
))}
</DraggableListWrapper>
<DraggableListWrapper
className="content"
onMouseDown={() => {
// set events to null to stop other parent draggable elements execution(ex: Property pane)
document.onmouseup = null;
document.onmousemove = null;
}}
style={{
height: "100%",
}}
>
{springs.map(({ scale, y, zIndex }, i) => (
<animated.div
{...bind(i)}
data-rbd-draggable-id={items[i].id}
key={i}
style={{
zIndex,
width: "100%",
transform: to(
[y, scale],
(y, s) => `translate3d(0,${y}px,0) scale(${s})`,
),
}}
>
<div>
<ItemRenderer index={i} item={items[i]} />
</div>
</animated.div>
))}
</DraggableListWrapper>
</div>
);
}
DraggableList.displayName = "DraggableList";

View File

@ -0,0 +1,162 @@
import React, { useCallback, useState, useRef, useEffect } from "react";
import styled from "styled-components";
import _ from "lodash";
import {
StyledDragIcon,
StyledOptionControlInputGroup,
StyledEditIcon,
StyledDeleteIcon,
StyledVisibleIcon,
StyledHiddenIcon,
} from "components/propertyControls/StyledControls";
import { Colors } from "constants/Colors";
const ItemWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
&.has-duplicate-label > div:nth-child(2) {
border: 1px solid ${Colors.DANGER_SOLID};
}
`;
type RenderComponentProps = {
focusedIndex: number | null | undefined;
index: number;
item: {
label: string;
isDerived?: boolean;
isVisible?: boolean;
isDuplicateLabel?: boolean;
};
isDelete?: boolean;
isDragging: boolean;
placeholder: string;
updateFocus?: (index: number, isFocused: boolean) => void;
updateOption: (index: number, value: string) => void;
onEdit?: (index: number) => void;
deleteOption: (index: number) => void;
toggleVisibility?: (index: number) => void;
};
export function DraggableListCard(props: RenderComponentProps) {
const [value, setValue] = useState(props.item.label);
const [isEditing, setEditing] = useState(false);
const {
deleteOption,
focusedIndex,
index,
isDelete,
isDragging,
item,
onEdit,
placeholder,
toggleVisibility,
updateFocus,
updateOption,
} = props;
const [visibility, setVisibility] = useState(item.isVisible);
const ref = useRef<HTMLInputElement | null>(null);
const debouncedUpdate = _.debounce(updateOption, 1000);
useEffect(() => {
if (!isEditing && item && item.label) setValue(item.label);
}, [item?.label, isEditing]);
useEffect(() => {
if (focusedIndex !== null && focusedIndex === index && !isDragging) {
if (ref && ref.current) {
ref?.current.focus();
}
} else if (isDragging && focusedIndex === index) {
if (ref && ref.current) {
ref?.current.blur();
}
}
}, [focusedIndex, isDragging]);
const onChange = useCallback(
(index: number, value: string) => {
setValue(value);
debouncedUpdate(index, value);
},
[updateOption],
);
const onFocus = () => {
setEditing(false);
if (updateFocus) {
updateFocus(index, true);
}
};
const onBlur = () => {
if (!isDragging) {
setEditing(false);
if (updateFocus) {
updateFocus(index, false);
}
}
};
return (
<ItemWrapper
className={props.item.isDuplicateLabel ? "has-duplicate-label" : ""}
>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
autoFocus={index === focusedIndex}
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder={placeholder}
ref={ref}
value={value}
width="100%"
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={() => {
onEdit && onEdit(index);
}}
width={20}
/>
{!!item.isDerived || isDelete ? (
<StyledDeleteIcon
className="t--delete-column-btn"
height={20}
onClick={() => {
deleteOption && deleteOption(index);
}}
width={20}
/>
) : visibility ? (
<StyledVisibleIcon
className="t--show-column-btn"
height={20}
onClick={() => {
setVisibility(!visibility);
toggleVisibility && toggleVisibility(index);
}}
width={20}
/>
) : (
<StyledHiddenIcon
className="t--show-column-btn"
height={20}
onClick={() => {
setVisibility(!visibility);
toggleVisibility && toggleVisibility(index);
}}
width={20}
/>
)}
</ItemWrapper>
);
}

View File

@ -3,11 +3,13 @@ import React from "react";
import DraggableList from "./DraggableList";
type RenderComponentProps = {
focusedIndex: number | null | undefined;
index: number;
item: {
label: string;
isDerived?: boolean;
};
isDragging: boolean;
deleteOption: (index: number) => void;
updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void;
@ -16,6 +18,8 @@ type RenderComponentProps = {
};
interface DroppableComponentProps {
fixedHeight?: number | boolean;
focusedIndex?: number | null | undefined;
items: Array<Record<string, unknown>>;
itemHeight: number;
renderComponent: (props: RenderComponentProps) => JSX.Element;
@ -34,11 +38,18 @@ export class DroppableComponent extends React.Component<
super(props);
}
shouldComponentUpdate(prevProps: DroppableComponentProps) {
public readonly state = {
isDragging: false,
};
shouldComponentUpdate(prevProps: DroppableComponentProps, prevState: any) {
const presentOrder = this.props.items.map(this.getVisibleObject);
const previousOrder = prevProps.items.map(this.getVisibleObject);
return !isEqual(presentOrder, previousOrder);
return (
!isEqual(presentOrder, previousOrder) ||
this.props.focusedIndex !== prevProps.focusedIndex ||
prevState.isDragging !== this.state.isDragging
);
}
getVisibleObject(item: Record<string, unknown>) {
@ -52,14 +63,26 @@ export class DroppableComponent extends React.Component<
};
}
onUpdate = (itemsOrder: number[]) => {
onUpdate = (
itemsOrder: number[],
originalIndex: number,
newIndex: number,
) => {
const newOrderedItems = itemsOrder.map((each) => this.props.items[each]);
this.props.updateItems(newOrderedItems);
if (this.props.updateFocus && originalIndex !== newIndex) {
this.props.updateFocus(newIndex, true);
}
};
updateDragging = (isDragging: boolean) => {
this.setState({ isDragging });
};
renderItem = ({ index, item }: any) => {
const {
deleteOption,
focusedIndex,
onEdit,
renderComponent,
toggleVisibility,
@ -73,8 +96,10 @@ export class DroppableComponent extends React.Component<
updateOption,
toggleVisibility,
onEdit,
focusedIndex,
item,
index,
isDragging: this.state.isDragging,
});
};
@ -82,10 +107,13 @@ export class DroppableComponent extends React.Component<
return (
<DraggableList
ItemRenderer={this.renderItem}
fixedHeight={this.props.fixedHeight}
focusedIndex={this.props.focusedIndex}
itemHeight={45}
items={this.props.items}
onUpdate={this.onUpdate}
shouldReRender={false}
updateDragging={this.updateDragging}
/>
);
}

View File

@ -17,6 +17,7 @@ type RenderComponentProps = {
isDerived?: boolean;
};
deleteOption: (index: number) => void;
updateCurrentFocusedInput: (index: number | null) => void;
updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void;
onEdit?: (index: number) => void;
@ -26,6 +27,7 @@ interface DroppableComponentProps {
items: Array<Record<string, unknown>>;
renderComponent: (props: RenderComponentProps) => JSX.Element;
deleteOption: (index: number) => void;
updateCurrentFocusedInput: (index: number | null) => void;
updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void;
updateItems: (items: Array<Record<string, unknown>>) => void;
@ -85,6 +87,7 @@ export class DroppableComponent extends React.Component<
onEdit,
renderComponent,
toggleVisibility,
updateCurrentFocusedInput,
updateOption,
} = this.props;
return (
@ -116,6 +119,7 @@ export class DroppableComponent extends React.Component<
>
{renderComponent({
deleteOption,
updateCurrentFocusedInput,
updateOption,
toggleVisibility,
onEdit,

View File

@ -1,20 +1,15 @@
import React, { useCallback, useEffect, useState } from "react";
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import {
StyledPropertyPaneButton,
StyledDragIcon,
StyledDeleteIcon,
StyledEditIcon,
StyledOptionControlInputGroup,
} from "./StyledControls";
import { StyledPropertyPaneButton } from "./StyledControls";
import styled from "constants/DefaultTheme";
import { generateReactKey } from "utils/generators";
import { DroppableComponent } from "components/ads/DraggableListComponent";
import { getNextEntityName } from "utils/AppsmithUtils";
import _, { debounce } from "lodash";
import _ from "lodash";
import { Category, Size } from "components/ads/Button";
import { Colors } from "constants/Colors";
import { ButtonPlacementTypes } from "components/constants";
import { DraggableListCard } from "components/ads/DraggableListCard";
const StyledPropertyPaneButtonWrapper = styled.div`
display: flex;
@ -23,12 +18,6 @@ const StyledPropertyPaneButtonWrapper = styled.div`
margin-top: 10px;
`;
const ButtonWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
`;
const ButtonListWrapper = styled.div`
width: 100%;
display: flex;
@ -40,77 +29,28 @@ const AddNewButton = styled(StyledPropertyPaneButton)`
flex-grow: 1;
`;
type RenderComponentProps = {
index: number;
item: {
label: string;
isVisible?: boolean;
};
deleteOption: (index: number) => void;
updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void;
onEdit?: (props: any) => void;
type State = {
focusedIndex: number | null;
};
function GroupButtonComponent(props: RenderComponentProps) {
const { deleteOption, index, item, updateOption } = props;
class ButtonListControl extends BaseControl<ControlProps, State> {
constructor(props: ControlProps) {
super(props);
const [value, setValue] = useState(item.label);
const [isEditing, setEditing] = useState(false);
this.state = {
focusedIndex: null,
};
}
useEffect(() => {
if (!isEditing && item && item.label) setValue(item.label);
}, [item?.label, isEditing]);
const debouncedUpdate = debounce(updateOption, 1000);
const onChange = useCallback(
(index: number, value: string) => {
setValue(value);
debouncedUpdate(index, value);
},
[updateOption],
);
const handleChange = useCallback(() => props.onEdit && props.onEdit(index), [
index,
]);
const onFocus = () => setEditing(true);
const onBlur = () => setEditing(false);
return (
<ButtonWrapper>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder="Button label"
trimValue={false}
value={value}
/>
<StyledDeleteIcon
className="t--delete-tab-btn"
height={20}
marginRight={12}
onClick={() => {
deleteOption(index);
}}
width={20}
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={handleChange}
width={20}
/>
</ButtonWrapper>
);
}
class ButtonListControl extends BaseControl<ControlProps> {
componentDidUpdate(prevProps: ControlProps): void {
//on adding a new column last column should get focused
if (
Object.keys(prevProps.propertyValue).length + 1 ===
Object.keys(this.props.propertyValue).length
) {
this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
}
}
updateItems = (items: Array<Record<string, any>>) => {
const menuItems = items.reduce((obj: any, each: any, index: number) => {
obj[each.id] = {
@ -146,11 +86,20 @@ class ButtonListControl extends BaseControl<ControlProps> {
<ButtonListWrapper>
<DroppableComponent
deleteOption={this.deleteOption}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45}
items={menuItems}
onEdit={this.onEdit}
renderComponent={GroupButtonComponent}
renderComponent={(props) =>
DraggableListCard({
...props,
isDelete: true,
placeholder: "Button label",
})
}
toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems}
updateOption={this.updateOption}
/>
@ -246,6 +195,10 @@ class ButtonListControl extends BaseControl<ControlProps> {
this.updateProperty(this.props.propertyName, groupButtons);
};
updateFocus = (index: number, isFocused: boolean) => {
this.setState({ focusedIndex: isFocused ? index : null });
};
static getControlType() {
return "GROUP_BUTTONS";
}

View File

@ -1,18 +1,13 @@
import React, { useCallback, useEffect, useState } from "react";
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import {
StyledPropertyPaneButton,
StyledDragIcon,
StyledDeleteIcon,
StyledEditIcon,
StyledOptionControlInputGroup,
} from "./StyledControls";
import { StyledPropertyPaneButton } from "./StyledControls";
import styled from "constants/DefaultTheme";
import { generateReactKey } from "utils/generators";
import { DroppableComponent } from "components/ads/DraggableListComponent";
import { getNextEntityName } from "utils/AppsmithUtils";
import _, { debounce, orderBy } from "lodash";
import _, { orderBy } from "lodash";
import { Category, Size } from "components/ads/Button";
import { DraggableListCard } from "components/ads/DraggableListCard";
const StyledPropertyPaneButtonWrapper = styled.div`
display: flex;
@ -21,12 +16,6 @@ const StyledPropertyPaneButtonWrapper = styled.div`
margin-top: 10px;
`;
const ItemWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
`;
const MenuItemsWrapper = styled.div`
width: 100%;
display: flex;
@ -38,77 +27,30 @@ const AddMenuItemButton = styled(StyledPropertyPaneButton)`
flex-grow: 1;
`;
type RenderComponentProps = {
index: number;
item: {
label: string;
isVisible?: boolean;
};
deleteOption: (index: number) => void;
updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void;
onEdit?: (props: any) => void;
type State = {
focusedIndex: number | null;
};
function MenuItemComponent(props: RenderComponentProps) {
const { deleteOption, index, item, updateOption } = props;
class MenuItemsControl extends BaseControl<ControlProps, State> {
constructor(props: ControlProps) {
super(props);
const [value, setValue] = useState(item.label);
const [isEditing, setEditing] = useState(false);
this.state = {
focusedIndex: null,
};
}
useEffect(() => {
if (!isEditing && item && item.label) setValue(item.label);
}, [item?.label, isEditing]);
const debouncedUpdate = debounce(updateOption, 1000);
const onChange = useCallback(
(index: number, value: string) => {
setValue(value);
debouncedUpdate(index, value);
},
[updateOption],
);
const handleChange = useCallback(() => props.onEdit && props.onEdit(index), [
index,
]);
const onFocus = () => setEditing(true);
const onBlur = () => setEditing(false);
return (
<ItemWrapper>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder="Menu item label"
trimValue={false}
value={value}
/>
<StyledDeleteIcon
className="t--delete-tab-btn"
height={20}
marginRight={12}
onClick={() => {
deleteOption(index);
}}
width={20}
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={handleChange}
width={20}
/>
</ItemWrapper>
);
}
class MenuItemsControl extends BaseControl<ControlProps> {
componentDidUpdate(prevProps: ControlProps): void {
//on adding a new column last column should get focused
if (
prevProps.propertyValue &&
this.props.propertyValue &&
Object.keys(prevProps.propertyValue).length + 1 ===
Object.keys(this.props.propertyValue).length
) {
this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
}
}
updateItems = (items: Array<Record<string, any>>) => {
const menuItems = items.reduce((obj: any, each: any, index: number) => {
obj[each.id] = {
@ -146,11 +88,20 @@ class MenuItemsControl extends BaseControl<ControlProps> {
<MenuItemsWrapper>
<DroppableComponent
deleteOption={this.deleteOption}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45}
items={orderBy(menuItems, ["index"], ["asc"])}
onEdit={this.onEdit}
renderComponent={MenuItemComponent}
renderComponent={(props) =>
DraggableListCard({
...props,
isDelete: true,
placeholder: "Menu item label",
})
}
toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems}
updateOption={this.updateOption}
/>
@ -243,6 +194,10 @@ class MenuItemsControl extends BaseControl<ControlProps> {
this.updateProperty(this.props.propertyName, menuItems);
};
updateFocus = (index: number, isFocused: boolean) => {
this.setState({ focusedIndex: isFocused ? index : null });
};
static getControlType() {
return "MENU_ITEMS";
}

View File

@ -1,19 +1,11 @@
import React, { useCallback, useEffect, useState, Component } from "react";
import React, { Component } from "react";
import { AppState } from "reducers";
import { connect } from "react-redux";
import { Placement } from "popper.js";
import * as Sentry from "@sentry/react";
import _ from "lodash";
import BaseControl, { ControlProps } from "./BaseControl";
import {
StyledDragIcon,
StyledEditIcon,
StyledDeleteIcon,
StyledVisibleIcon,
StyledHiddenIcon,
StyledPropertyPaneButton,
StyledOptionControlInputGroup,
} from "./StyledControls";
import { StyledPropertyPaneButton } from "./StyledControls";
import styled from "constants/DefaultTheme";
import { Indices } from "constants/Layers";
import { DroppableComponent } from "components/ads/DraggableListComponent";
@ -37,17 +29,7 @@ import {
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import { getNextEntityName } from "utils/AppsmithUtils";
import { Colors } from "constants/Colors";
import { noop } from "utils/AppsmithUtils";
const ItemWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
&.has-duplicate-label > div:nth-child(2) {
border: 1px solid ${Colors.DANGER_SOLID};
}
`;
import { DraggableListCard } from "components/ads/DraggableListCard";
const TabsWrapper = styled.div`
width: 100%;
@ -83,21 +65,6 @@ type EvaluatedValuePopupWrapperProps = ReduxStateProps & {
children: JSX.Element;
};
type RenderComponentProps = {
index: number;
item: {
label: string;
isDerived?: boolean;
isVisible?: boolean;
isDuplicateLabel?: boolean;
};
updateFocus?: (index: number, isFocused: boolean) => void;
updateOption: (index: number, value: string) => void;
onEdit?: (index: number) => void;
deleteOption: (index: number) => void;
toggleVisibility?: (index: number) => void;
};
const getOriginalColumn = (
columns: Record<string, ColumnProperties>,
index: number,
@ -110,107 +77,6 @@ const getOriginalColumn = (
return column;
};
function ColumnControlComponent(props: RenderComponentProps) {
const [value, setValue] = useState(props.item.label);
const [isEditing, setEditing] = useState(false);
useEffect(() => {
if (!isEditing && props.item && props.item.label)
setValue(props.item.label);
}, [props.item?.label, isEditing]);
const {
deleteOption,
index,
item,
onEdit,
toggleVisibility,
updateFocus,
updateOption,
} = props;
const [visibility, setVisibility] = useState(item.isVisible);
useEffect(() => {
setVisibility(item.isVisible);
}, [item.isVisible]);
const debouncedUpdate = _.debounce(updateOption, 1000);
const debouncedFocus = updateFocus ? _.debounce(updateFocus, 400) : noop;
const onChange = useCallback(
(index: number, value: string) => {
setValue(value);
debouncedUpdate(index, value);
},
[updateOption],
);
const onFocus = () => {
setEditing(false);
debouncedFocus(index, true);
};
const onBlur = () => {
setEditing(false);
debouncedFocus(index, false);
};
return (
<ItemWrapper
className={props.item.isDuplicateLabel ? "has-duplicate-label" : ""}
>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder="Column Title"
trimValue={false}
value={value}
width="100%"
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={() => {
onEdit && onEdit(index);
}}
width={20}
/>
{!!item.isDerived ? (
<StyledDeleteIcon
className="t--delete-column-btn"
height={20}
onClick={() => {
deleteOption && deleteOption(index);
}}
width={20}
/>
) : visibility ? (
<StyledVisibleIcon
className="t--show-column-btn"
height={20}
onClick={() => {
setVisibility(!visibility);
toggleVisibility && toggleVisibility(index);
}}
width={20}
/>
) : (
<StyledHiddenIcon
className="t--show-column-btn"
height={20}
onClick={() => {
setVisibility(!visibility);
toggleVisibility && toggleVisibility(index);
}}
width={20}
/>
)}
</ItemWrapper>
);
}
type State = {
focusedIndex: number | null;
duplicateColumnIds: string[];
@ -241,6 +107,16 @@ class PrimaryColumnsControl extends BaseControl<ControlProps, State> {
};
}
componentDidUpdate(prevProps: ControlProps): void {
//on adding a new column last column should get focused
if (
Object.keys(prevProps.propertyValue).length + 1 ===
Object.keys(this.props.propertyValue).length
) {
this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
}
}
render() {
// Get columns from widget properties
const columns: Record<string, ColumnProperties> =
@ -291,10 +167,18 @@ class PrimaryColumnsControl extends BaseControl<ControlProps, State> {
<EvaluatedValuePopupWrapper {...this.props} isFocused={isFocused}>
<DroppableComponent
deleteOption={this.deleteOption}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45}
items={draggableComponentColumns}
onEdit={this.onEdit}
renderComponent={ColumnControlComponent}
renderComponent={(props) =>
DraggableListCard({
...props,
isDelete: false,
placeholder: "Column Title",
})
}
toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems}
@ -445,6 +329,8 @@ class PrimaryColumnsControl extends BaseControl<ControlProps, State> {
this.setState({ focusedIndex: isFocused ? index : null });
};
// updateCurrentFocusedInput = (index: number | null) => {};
static getControlType() {
return "PRIMARY_COLUMNS";
}

View File

@ -1,21 +1,16 @@
import React, { useCallback, useEffect, useState } from "react";
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import {
StyledPropertyPaneButton,
StyledDragIcon,
StyledDeleteIcon,
StyledEditIcon,
StyledOptionControlInputGroup,
} from "./StyledControls";
import { StyledPropertyPaneButton } from "./StyledControls";
import styled from "constants/DefaultTheme";
import { generateReactKey } from "utils/generators";
import { DroppableComponent } from "components/ads/DraggableListComponent";
import { getNextEntityName, noop } from "utils/AppsmithUtils";
import _, { debounce, orderBy } from "lodash";
import _, { orderBy } from "lodash";
import * as Sentry from "@sentry/react";
import { Category, Size } from "components/ads/Button";
import { useDispatch } from "react-redux";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { DraggableListCard } from "components/ads/DraggableListCard";
const StyledPropertyPaneButtonWrapper = styled.div`
display: flex;
@ -24,12 +19,6 @@ const StyledPropertyPaneButtonWrapper = styled.div`
margin-top: 10px;
`;
const ItemWrapper = styled.div`
display: flex;
justify-content: flex-start;
align-items: center;
`;
const TabsWrapper = styled.div`
width: 100%;
display: flex;
@ -37,12 +26,15 @@ const TabsWrapper = styled.div`
`;
type RenderComponentProps = {
focusedIndex: number | null | undefined;
index: number;
isDragging: boolean;
item: {
label: string;
isVisible?: boolean;
};
deleteOption: (index: number) => void;
updateFocus?: (index: number, isFocused: boolean) => void;
updateOption: (index: number, value: string) => void;
toggleVisibility?: (index: number) => void;
onEdit?: (props: any) => void;
@ -74,7 +66,7 @@ function AddTabButtonComponent({ widgetId }: any) {
}
function TabControlComponent(props: RenderComponentProps) {
const { index, item, updateOption } = props;
const { index, item } = props;
const dispatch = useDispatch();
const deleteOption = () => {
dispatch({
@ -83,65 +75,42 @@ function TabControlComponent(props: RenderComponentProps) {
});
};
const [value, setValue] = useState(item.label);
const [isEditing, setEditing] = useState(false);
useEffect(() => {
if (!isEditing && item && item.label) setValue(item.label);
}, [item?.label, isEditing]);
const debouncedUpdate = debounce(updateOption, 1000);
const handleChange = useCallback(() => props.onEdit && props.onEdit(index), [
index,
]);
const onChange = useCallback(
(index: number, value: string) => {
setValue(value);
debouncedUpdate(index, value);
},
[updateOption],
);
const onFocus = () => setEditing(true);
const onBlur = () => setEditing(false);
return (
<ItemWrapper>
<StyledDragIcon height={20} width={20} />
<StyledOptionControlInputGroup
dataType="text"
onBlur={onBlur}
onChange={(value: string) => {
onChange(index, value);
}}
onFocus={onFocus}
placeholder="Tab Title"
trimValue={false}
value={value}
/>
<StyledDeleteIcon
className="t--delete-tab-btn"
height={20}
marginRight={12}
onClick={deleteOption}
width={20}
/>
<StyledEditIcon
className="t--edit-column-btn"
height={20}
onClick={handleChange}
width={20}
/>
</ItemWrapper>
<DraggableListCard
{...props}
deleteOption={deleteOption}
isDelete
placeholder="Tab Title"
/>
);
}
class TabControl extends BaseControl<ControlProps> {
type State = {
focusedIndex: number | null;
};
class TabControl extends BaseControl<ControlProps, State> {
constructor(props: ControlProps) {
super(props);
this.state = {
focusedIndex: null,
};
}
componentDidMount() {
this.migrateTabData(this.props.propertyValue);
}
componentDidUpdate(prevProps: ControlProps): void {
//on adding a new column last column should get focused
if (
Object.keys(prevProps.propertyValue).length + 1 ===
Object.keys(this.props.propertyValue).length
) {
this.updateFocus(Object.keys(this.props.propertyValue).length - 1, true);
}
}
migrateTabData(
tabData: Array<{
id: string;
@ -204,11 +173,14 @@ class TabControl extends BaseControl<ControlProps> {
<TabsWrapper>
<DroppableComponent
deleteOption={noop}
fixedHeight={370}
focusedIndex={this.state.focusedIndex}
itemHeight={45}
items={orderBy(tabs, ["index"], ["asc"])}
onEdit={this.onEdit}
renderComponent={TabControlComponent}
toggleVisibility={this.toggleVisibility}
updateFocus={this.updateFocus}
updateItems={this.updateItems}
updateOption={this.updateOption}
/>
@ -269,6 +241,10 @@ class TabControl extends BaseControl<ControlProps> {
this.updateProperty(this.props.propertyName, tabs);
};
updateFocus = (index: number, isFocused: boolean) => {
this.setState({ focusedIndex: isFocused ? index : null });
};
static getControlType() {
return "TABS_INPUT";
}