diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index 971365a177..eeaa18324a 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -80,10 +80,7 @@ export const moveActionRequest = (payload: { }; }; -export const moveActionSuccess = (payload: { - id: string; - destinationPageId: string; -}) => { +export const moveActionSuccess = (payload: RestAction) => { return { type: ReduxActionTypes.MOVE_ACTION_SUCCESS, payload, diff --git a/app/client/src/components/editorComponents/ContextDropdown.tsx b/app/client/src/components/editorComponents/ContextDropdown.tsx index 2c71783825..d31c7dff5f 100644 --- a/app/client/src/components/editorComponents/ContextDropdown.tsx +++ b/app/client/src/components/editorComponents/ContextDropdown.tsx @@ -6,6 +6,7 @@ import { MenuItem, Intent as BlueprintIntent, PopoverPosition, + PopoverInteractionKind, } from "@blueprintjs/core"; import { DropdownOption } from "widgets/DropdownWidget"; import { ControlIconName, ControlIcons } from "icons/ControlIcons"; @@ -51,7 +52,9 @@ const DropdownItem = (option: ContextDropdownOption) => ( popoverProps={{ minimal: true, hoverCloseDelay: 0, - hoverOpenDelay: 0, + hoverOpenDelay: 300, + interactionKind: PopoverInteractionKind.CLICK, + position: PopoverPosition.RIGHT, modifiers: { arrow: { enabled: false, @@ -84,9 +87,6 @@ export const ContextDropdown = (props: ContextDropdownProps) => { return ( ; }; -class DynamicAutocompleteInput extends Component { +type State = { + isFocused: boolean; +}; + +class DynamicAutocompleteInput extends Component { textArea = React.createRef(); editor: any; + constructor(props: Props) { + super(props); + this.state = { + isFocused: false, + }; + } + componentDidMount(): void { if (this.textArea.current) { const options: EditorConfiguration = {}; @@ -168,6 +179,8 @@ class DynamicAutocompleteInput extends Component { }); this.editor.on("change", _.debounce(this.handleChange, 100)); this.editor.on("cursorActivity", this.handleAutocompleteVisibility); + this.editor.on("focus", () => this.setState({ isFocused: true })); + this.editor.on("blur", () => this.setState({ isFocused: false })); this.editor.setOption("hintOptions", { completeSingle: false, globalScope: this.props.dynamicData, @@ -179,10 +192,11 @@ class DynamicAutocompleteInput extends Component { } } - componentDidUpdate(): void { + componentDidUpdate(prevProps: Props): void { if (this.editor) { const editorValue = this.editor.getValue(); let inputValue = this.props.input.value; + // Safe update of value of the editor when value updated outside the editor if (typeof inputValue === "object") { inputValue = JSON.stringify(inputValue, null, 2); } @@ -191,6 +205,13 @@ class DynamicAutocompleteInput extends Component { this.editor.setValue(inputValue); this.editor.setCursor(cursor); } + // Update the dynamic bindings for autocomplete + if (prevProps.dynamicData !== this.props.dynamicData) { + this.editor.setOption("hintOptions", { + completeSingle: false, + globalScope: this.props.dynamicData, + }); + } } } @@ -253,7 +274,7 @@ class DynamicAutocompleteInput extends Component { const hasError = !!(meta && meta.error); let showError = false; if (this.editor) { - showError = hasError && this.editor.hasFocus(); + showError = hasError && this.state.isFocused; } return ( diff --git a/app/client/src/components/editorComponents/ErrorBoundry.tsx b/app/client/src/components/editorComponents/ErrorBoundry.tsx index 8338e58e8b..d052642217 100644 --- a/app/client/src/components/editorComponents/ErrorBoundry.tsx +++ b/app/client/src/components/editorComponents/ErrorBoundry.tsx @@ -1,8 +1,14 @@ import React from "react"; +import styled from "styled-components"; type Props = {}; type State = { hasError: boolean }; +const RetryLink = styled.span` + color: ${props => props.theme.colors.primaryDarkest}; + cursor: pointer; +`; + class ErrorBoundary extends React.Component { constructor(props: Props) { super(props); @@ -20,8 +26,15 @@ class ErrorBoundary extends React.Component { render() { if (this.state.hasError) { - // You can render any custom fallback UI - return

Oops, Something went wrong.

; + return ( +

+ Oops, Something went wrong. +
+ this.setState({ hasError: false })}> + Click here to retry + +

+ ); } return this.props.children; diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index aead74f243..170fe412b2 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -33,6 +33,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { rows: 1, columns: 8, widgetName: "Input", + text: "", }, SWITCH_WIDGET: { isOn: false, @@ -115,6 +116,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { { id: "3", label: "Option 3", value: "3" }, ], widgetName: "Dropdown", + selectedIndex: 0, }, CHECKBOX_WIDGET: { rows: 1, diff --git a/app/client/src/pages/Editor/APIEditor/Form.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx index f72556c285..2e7097344c 100644 --- a/app/client/src/pages/Editor/APIEditor/Form.tsx +++ b/app/client/src/pages/Editor/APIEditor/Form.tsx @@ -110,7 +110,7 @@ const ApiEditorForm: React.FC = (props: Props) => { diff --git a/app/client/src/pages/Editor/ApiSidebar.tsx b/app/client/src/pages/Editor/ApiSidebar.tsx index 33456d9483..335d23b015 100644 --- a/app/client/src/pages/Editor/ApiSidebar.tsx +++ b/app/client/src/pages/Editor/ApiSidebar.tsx @@ -112,7 +112,7 @@ class ApiSidebar extends React.Component { .map(a => a.name); let name = action.name; if (pageApiNames.indexOf(action.name) > -1) { - name = getNextEntityName(`${name}-`, pageApiNames); + name = getNextEntityName(name, pageApiNames); } this.props.moveAction(itemId, destinationPageId, name, action.pageId); }; @@ -124,7 +124,7 @@ class ApiSidebar extends React.Component { .map(a => a.name); let name = `${action.name}Copy`; if (pageApiNames.indexOf(name) > -1) { - name = getNextEntityName(`${name}`, pageApiNames); + name = getNextEntityName(name, pageApiNames); } this.props.copyAction(itemId, destinationPageId, name); }; diff --git a/app/client/src/pages/Editor/EditorSidebar.tsx b/app/client/src/pages/Editor/EditorSidebar.tsx index 6138828571..f62aaa0db2 100644 --- a/app/client/src/pages/Editor/EditorSidebar.tsx +++ b/app/client/src/pages/Editor/EditorSidebar.tsx @@ -60,29 +60,34 @@ const PageName = styled.h5<{ isMain: boolean }>` width: 100%; height: 20px; padding-left: 5px; - font-size: 13px; + font-size: 16px; color: white; border-right: 4px solid; + margin: 10px 0; border-color: ${props => props.isMain ? props.theme.colors.primary : "transparent"}; `; -const PageDropContainer = styled.div<{ isActive: boolean }>` +const PageDropContainer = styled.div` min-height: 32px; margin: 5px; - background-color: ${props => - props.isActive ? props.theme.colors.paneCard : props.theme.colors.paneBG}; + background-color: ${props => props.theme.colors.paneBG}; `; const ItemsWrapper = styled.div` flex: 1; `; -const ItemRenderContainer = styled.div` - flex: 1; +const NoItemMessage = styled.span` + color: #cacaca; + padding-left: 12px; `; -const ItemContainer = styled.div<{ isSelected: boolean }>` +const ItemContainer = styled.div<{ + isSelected: boolean; + isDraggingOver: boolean; + isBeingDragged: boolean; +}>` height: 32px; width: 100%; padding: 8px 12px; @@ -94,9 +99,14 @@ const ItemContainer = styled.div<{ isSelected: boolean }>` align-items: center; justify-content: space-between; background-color: ${props => - props.isSelected ? props.theme.colors.paneCard : props.theme.colors.paneBG} + props.isSelected || props.isBeingDragged + ? props.theme.colors.paneCard + : props.theme.colors.paneBG} :hover { - background-color: ${props => props.theme.colors.paneCard}; + background-color: ${props => + props.isDraggingOver + ? props.theme.colors.paneBG + : props.theme.colors.paneCard}; } `; @@ -299,98 +309,106 @@ class EditorSidebar extends React.Component { {provided.placeholder} -
+
+ {page.items.length === 0 && ( + + {"No item on this page yet"} + + )} {page.items.map((item: Item, index) => ( - - - {provided => ( - - this.handleItemSelect(item.id) - } - > - {itemRender(item)} - - )} - - {this.state.itemDragging !== item.id && ( - - - null, - label: "Copy to", - children: pageWiseList.map(p => ({ - label: p.name, - id: p.id, - value: p.name, - onSelect: () => - this.props.copyItem( - item.id, - p.id, + {provided => ( + + this.handleItemSelect(item.id) + } + > + {itemRender(item)} + {this.state.itemDragging !== + item.id && ( + + + null, + label: "Copy to", + children: pageWiseList.map( + p => ({ + label: p.name, + id: p.id, + value: p.name, + onSelect: () => + this.props.copyItem( + item.id, + p.id, + ), + }), ), - })), - }, - { - id: "move", - value: "move", - onSelect: () => null, - label: "Move to", - children: pageWiseList - .filter(p => p.id !== page.id) - .map(p => ({ - label: p.name, - id: p.id, - value: p.name, + }, + { + id: "move", + value: "move", + onSelect: () => null, + label: "Move to", + children: pageWiseList + .filter( + p => p.id !== page.id, + ) + .map(p => ({ + label: p.name, + id: p.id, + value: p.name, + onSelect: () => + this.props.moveItem( + item.id, + p.id, + ), + })), + }, + { + id: "delete", + value: "delete", onSelect: () => - this.props.moveItem( + this.props.deleteItem( item.id, - p.id, ), - })), - }, - { - id: "delete", - value: "delete", - onSelect: () => - this.props.deleteItem(item.id), - label: "Delete", - intent: "danger", - }, - ]} - toggle={{ - type: "icon", - icon: "MORE_HORIZONTAL_CONTROL", - iconSize: theme.fontSizes[4], - }} - className="more" - /> - + label: "Delete", + intent: "danger", + }, + ]} + toggle={{ + type: "icon", + icon: "MORE_HORIZONTAL_CONTROL", + iconSize: theme.fontSizes[4], + }} + className="more" + /> + + )} + )} - + ))}
diff --git a/app/client/src/pages/Editor/routes.tsx b/app/client/src/pages/Editor/routes.tsx index 89d66a75f1..a2c4d715ec 100644 --- a/app/client/src/pages/Editor/routes.tsx +++ b/app/client/src/pages/Editor/routes.tsx @@ -25,8 +25,6 @@ const Wrapper = styled.div<{ isVisible: boolean; showOnlySidebar?: boolean }>` background-color: ${props => props.isVisible ? "rgba(0, 0, 0, 0.26)" : "transparent"}; z-index: ${props => (props.isVisible ? 2 : -1)}; - transition-property: z-index; - transition-delay: ${props => (props.isVisible ? "0" : "0.25s")}; `; const DrawerWrapper = styled.div<{ @@ -37,7 +35,6 @@ const DrawerWrapper = styled.div<{ width: ${props => (props.showOnlySidebar ? "0px" : "75%")}; height: 100%; box-shadow: -1px 2px 3px 0px ${props => props.theme.colors.paneBG}; - transition: 0.25s; `; interface RouterState { diff --git a/app/client/src/reducers/entityReducers/actionsReducer.tsx b/app/client/src/reducers/entityReducers/actionsReducer.tsx index 203f06bdec..2f68bfc1af 100644 --- a/app/client/src/reducers/entityReducers/actionsReducer.tsx +++ b/app/client/src/reducers/entityReducers/actionsReducer.tsx @@ -105,6 +105,18 @@ const actionsReducer = createReducer(initialState, { return restAction; }), }), + [ReduxActionTypes.MOVE_ACTION_SUCCESS]: ( + state: ActionDataState, + action: ReduxAction, + ) => ({ + ...state, + data: state.data.map(restAction => { + if (restAction.id === action.payload.id) { + return action.payload; + } + return restAction; + }), + }), [ReduxActionErrorTypes.MOVE_ACTION_ERROR]: ( state: ActionDataState, action: ReduxAction<{ id: string; originalPageId: string }>, diff --git a/app/client/src/reducers/entityReducers/datasourceReducer.ts b/app/client/src/reducers/entityReducers/datasourceReducer.ts index c68f10a627..452df486bf 100644 --- a/app/client/src/reducers/entityReducers/datasourceReducer.ts +++ b/app/client/src/reducers/entityReducers/datasourceReducer.ts @@ -1,5 +1,9 @@ import { createReducer } from "utils/AppsmithUtils"; -import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; +import { + ReduxActionTypes, + ReduxAction, + ReduxActionErrorTypes, +} from "constants/ReduxActionConstants"; import { Datasource } from "api/DatasourcesApi"; export interface DatasourceDataState { @@ -39,6 +43,14 @@ const datasourceReducer = createReducer(initialState, { list: state.list.concat(action.payload), }; }, + [ReduxActionErrorTypes.CREATE_DATASOURCE_ERROR]: ( + state: DatasourceDataState, + ) => { + return { + ...state, + loading: false, + }; + }, }); export default datasourceReducer; diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index e67e43da10..382df5545c 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -45,6 +45,7 @@ import { getDynamicBindings, getDynamicValue, isDynamicValue, + removeBindingsFromObject, } from "utils/DynamicBindingUtils"; import { validateResponse } from "./ErrorSagas"; import { getDataTree } from "selectors/entitiesSelector"; @@ -418,9 +419,13 @@ function* moveActionSaga( const actionObject: RestAction = dirty ? drafts[action.payload.id] : yield select(getAction, action.payload.id); + const withoutBindings = removeBindingsFromObject(actionObject); try { const response = yield ActionAPI.moveAction({ - action: { ...actionObject, name: action.payload.name }, + action: { + ...withoutBindings, + name: action.payload.name, + }, destinationPageId: action.payload.destinationPageId, }); @@ -431,7 +436,7 @@ function* moveActionSaga( intent: Intent.SUCCESS, }); } - yield put(moveActionSuccess(action.payload)); + yield put(moveActionSuccess(response.data)); } catch (e) { AppToaster.show({ message: `Error while moving action ${actionObject.name}`, @@ -451,9 +456,12 @@ function* copyActionSaga( ) { const drafts = yield select(state => state.ui.apiPane.drafts); const dirty = action.payload.id in drafts; - const actionObject = dirty + let actionObject = dirty ? drafts[action.payload.id] : yield select(getAction, action.payload.id); + if (action.payload.destinationPageId !== actionObject.pageId) { + actionObject = removeBindingsFromObject(actionObject); + } try { const copyAction = { ...(_.omit(actionObject, "id") as RestAction), diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts index 0e98fbb91e..12796f8380 100644 --- a/app/client/src/sagas/ApiPaneSagas.ts +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -238,6 +238,18 @@ function* handleActionDeletedSaga(actionPayload: ReduxAction<{ id: string }>) { }); } +function* handleMoveOrCopySaga(actionPayload: ReduxAction<{ id: string }>) { + const { id } = actionPayload.payload; + const action = yield select(getAction, id); + const { values }: { values: RestAction } = yield select( + getFormData, + API_EDITOR_FORM_NAME, + ); + if (values.id === id) { + yield put(initialize(API_EDITOR_FORM_NAME, action)); + } +} + export default function* root() { yield all([ takeEvery(ReduxActionTypes.INIT_API_PANE, initApiPaneSaga), @@ -245,6 +257,8 @@ export default function* root() { takeEvery(ReduxActionTypes.CREATE_ACTION_SUCCESS, handleActionCreatedSaga), takeEvery(ReduxActionTypes.UPDATE_ACTION_SUCCESS, handleActionUpdatedSaga), takeEvery(ReduxActionTypes.DELETE_ACTION_SUCCESS, handleActionDeletedSaga), + takeEvery(ReduxActionTypes.MOVE_ACTION_SUCCESS, handleMoveOrCopySaga), + takeEvery(ReduxActionTypes.COPY_ACTION_SUCCESS, handleMoveOrCopySaga), // Intercepting the redux-form change actionType takeEvery(ReduxFormActionTypes.VALUE_CHANGE, formValueChangeSaga), takeEvery(ReduxFormActionTypes.ARRAY_REMOVE, formValueChangeSaga), diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index 6377184d19..59e998b375 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -38,7 +38,8 @@ function* createDatasourceSaga( const response: GenericApiResponse = yield DatasourcesApi.createDatasource( actionPayload.payload, ); - if (response.responseMeta.success) { + const isValidResponse = yield validateResponse(response); + if (isValidResponse) { yield put({ type: ReduxActionTypes.CREATE_DATASOURCE_SUCCESS, payload: response.data, @@ -49,7 +50,7 @@ function* createDatasourceSaga( } } catch (error) { yield put({ - type: ReduxActionTypes.CREATE_DATASOURCE_ERROR, + type: ReduxActionErrorTypes.CREATE_DATASOURCE_ERROR, payload: { error }, }); } diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 59069b5dae..13e41dfc64 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -7,6 +7,12 @@ import unescapeJS from "unescape-js"; import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector"; import toposort from "toposort"; +export const removeBindingsFromObject = (obj: object) => { + const string = JSON.stringify(obj); + const withBindings = string.replace(DATA_BIND_REGEX, "{{ }}"); + return JSON.parse(withBindings); +}; + export const isDynamicValue = (value: string): boolean => DATA_BIND_REGEX.test(value); @@ -267,7 +273,7 @@ export function dependencySortedEvaluateDataTree( dependencyTree: Array<[string, string]>, parseValues: boolean, ) { - const tree = JSON.parse(JSON.stringify(dataTree)); + const tree = _.cloneDeep(dataTree); try { // sort dependencies const sortedDependencies = toposort(dependencyTree).reverse(); diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index 149fc385d1..d03f8e14c0 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -183,7 +183,7 @@ abstract class BaseWidget< this.props.widgetId === "0" } > - {this.getPageView()} + {this.getPageView()} ); } diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx index fba9858a53..6b0fa12755 100644 --- a/app/client/src/widgets/DropdownWidget.tsx +++ b/app/client/src/widgets/DropdownWidget.tsx @@ -16,7 +16,7 @@ class DropdownWidget extends BaseWidget { return { placeholderText: VALIDATION_TYPES.TEXT, label: VALIDATION_TYPES.TEXT, - options: VALIDATION_TYPES.ARRAY, + options: VALIDATION_TYPES.TABLE_DATA, selectionType: VALIDATION_TYPES.TEXT, selectedIndex: VALIDATION_TYPES.NUMBER, selectedIndexArr: VALIDATION_TYPES.ARRAY,