Various P0 fixes

This commit is contained in:
Hetu Nandu 2020-01-27 13:53:33 +00:00
parent a3d85b8b3a
commit 9b00a345ad
17 changed files with 220 additions and 119 deletions

View File

@ -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,

View File

@ -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 (
<Dropdown
popoverProps={{
position: PopoverPosition.AUTO_END,
}}
items={props.options}
itemRenderer={renderer}
onItemSelect={noop}

View File

@ -141,10 +141,21 @@ type Props = ReduxStateProps &
input: Partial<WrappedFieldInputProps>;
};
class DynamicAutocompleteInput extends Component<Props> {
type State = {
isFocused: boolean;
};
class DynamicAutocompleteInput extends Component<Props, State> {
textArea = React.createRef<HTMLTextAreaElement>();
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<Props> {
});
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<Props> {
}
}
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<Props> {
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<Props> {
const hasError = !!(meta && meta.error);
let showError = false;
if (this.editor) {
showError = hasError && this.editor.hasFocus();
showError = hasError && this.state.isFocused;
}
return (
<ErrorTooltip message={meta ? meta.error : ""} isOpen={showError}>

View File

@ -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<Props, State> {
constructor(props: Props) {
super(props);
@ -20,8 +26,15 @@ class ErrorBoundary extends React.Component<Props, State> {
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <p>Oops, Something went wrong.</p>;
return (
<p>
Oops, Something went wrong.
<br />
<RetryLink onClick={() => this.setState({ hasError: false })}>
Click here to retry
</RetryLink>
</p>
);
}
return this.props.children;

View File

@ -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,

View File

@ -110,7 +110,7 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
<FormRow>
<TextField
name="name"
placeholder="API name (camel case)"
placeholder="nameOfApi (camel case)"
showError
/>
<ActionButtons>

View File

@ -112,7 +112,7 @@ class ApiSidebar extends React.Component<Props> {
.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<Props> {
.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);
};

View File

@ -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<Props, State> {
<PageDropContainer
ref={provided.innerRef}
{...provided.droppableProps}
isActive={snapshot.isDraggingOver}
>
{provided.placeholder}
<div
style={{
opacity: snapshot.isDraggingOver ? 0 : 1,
}}
>
<div>
{page.items.length === 0 && (
<NoItemMessage>
{"No item on this page yet"}
</NoItemMessage>
)}
{page.items.map((item: Item, index) => (
<ItemContainer
<Draggable
key={item.id}
isSelected={item.id === selectedItemId}
draggableId={item.id}
index={index}
>
<Draggable
draggableId={item.id}
index={index}
>
{provided => (
<ItemRenderContainer
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
onClick={() =>
this.handleItemSelect(item.id)
}
>
{itemRender(item)}
</ItemRenderContainer>
)}
</Draggable>
{this.state.itemDragging !== item.id && (
<React.Fragment>
<DraftIconIndicator
isHidden={
draftIds.indexOf(item.id) === -1
}
/>
<ContextDropdown
options={[
{
id: "copy",
value: "copy",
onSelect: () => 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 => (
<ItemContainer
isSelected={item.id === selectedItemId}
isDraggingOver={snapshot.isDraggingOver}
isBeingDragged={
this.state.itemDragging === item.id
}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
onClick={() =>
this.handleItemSelect(item.id)
}
>
{itemRender(item)}
{this.state.itemDragging !==
item.id && (
<React.Fragment>
<DraftIconIndicator
isHidden={
draftIds.indexOf(item.id) === -1
}
/>
<ContextDropdown
options={[
{
id: "copy",
value: "copy",
onSelect: () => 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"
/>
</React.Fragment>
label: "Delete",
intent: "danger",
},
]}
toggle={{
type: "icon",
icon: "MORE_HORIZONTAL_CONTROL",
iconSize: theme.fontSizes[4],
}}
className="more"
/>
</React.Fragment>
)}
</ItemContainer>
)}
</ItemContainer>
</Draggable>
))}
</div>
</PageDropContainer>

View File

@ -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 {

View File

@ -105,6 +105,18 @@ const actionsReducer = createReducer(initialState, {
return restAction;
}),
}),
[ReduxActionTypes.MOVE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
) => ({
...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 }>,

View File

@ -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;

View File

@ -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),

View File

@ -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),

View File

@ -38,7 +38,8 @@ function* createDatasourceSaga(
const response: GenericApiResponse<Datasource> = 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 },
});
}

View File

@ -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();

View File

@ -183,7 +183,7 @@ abstract class BaseWidget<
this.props.widgetId === "0"
}
>
{this.getPageView()}
<ErrorBoundary>{this.getPageView()}</ErrorBoundary>
</PositionedContainer>
);
}

View File

@ -16,7 +16,7 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
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,