Applications Page Styling
This commit is contained in:
parent
d089341df1
commit
2fa79e403d
|
|
@ -14,6 +14,7 @@
|
|||
"@blueprintjs/timezone": "^3.6.0",
|
||||
"@craco/craco": "^5.6.1",
|
||||
"@sentry/browser": "^5.6.3",
|
||||
"@types/faker": "^4.1.7",
|
||||
"@types/fontfaceobserver": "^0.0.6",
|
||||
"@types/lodash": "^4.14.120",
|
||||
"@types/moment-timezone": "^0.5.10",
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
"@uppy/webcam": "^1.3.1",
|
||||
"axios": "^0.18.0",
|
||||
"eslint": "^6.4.0",
|
||||
"faker": "^4.1.0",
|
||||
"flow-bin": "^0.91.0",
|
||||
"fontfaceobserver": "^2.1.0",
|
||||
"fuse.js": "^3.4.5",
|
||||
|
|
|
|||
13
app/client/src/assets/icons/control/more-vertical.svg
Executable file
13
app/client/src/assets/icons/control/more-vertical.svg
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon/Outline/more-vertical">
|
||||
<path id="Mask" fill-rule="evenodd" clip-rule="evenodd" d="M12 7C13.104 7 14 6.104 14 5C14 3.896 13.104 3 12 3C10.896 3 10 3.896 10 5C10 6.104 10.896 7 12 7ZM12 10C10.896 10 10 10.896 10 12C10 13.104 10.896 14 12 14C13.104 14 14 13.104 14 12C14 10.896 13.104 10 12 10ZM10 19C10 17.896 10.896 17 12 17C13.104 17 14 17.896 14 19C14 20.104 13.104 21 12 21C10.896 21 10 20.104 10 19Z" fill="#A3B3BF"/>
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="10" y="3" width="4" height="18">
|
||||
<path id="Mask_2" fill-rule="evenodd" clip-rule="evenodd" d="M12 7C13.104 7 14 6.104 14 5C14 3.896 13.104 3 12 3C10.896 3 10 3.896 10 5C10 6.104 10.896 7 12 7ZM12 10C10.896 10 10 10.896 10 12C10 13.104 10.896 14 12 14C13.104 14 14 13.104 14 12C14 10.896 13.104 10 12 10ZM10 19C10 17.896 10.896 17 12 17C13.104 17 14 17.896 14 19C14 20.104 13.104 21 12 21C10.896 21 10 20.104 10 19Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<g id="🎨 Color">
|
||||
<rect id="Base" width="24" height="24" fill="#A3B3BF"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,5 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="10" fill="#21282C"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 10.9513C9.47555 10.9513 9.04876 10.5245 9.04876 10C9.04876 9.47556 9.47555 9.04876 10 9.04876C10.5245 9.04876 10.9513 9.47556 10.9513 10C10.9513 10.5245 10.5245 10.9513 10 10.9513ZM10 7.78042C8.77606 7.78042 7.78041 8.77607 7.78041 10C7.78041 11.224 8.77606 12.2196 10 12.2196C11.224 12.2196 12.2196 11.224 12.2196 10C12.2196 8.77607 11.224 7.78042 10 7.78042ZM10.1393 13.1694C7.4086 13.2328 5.62721 10.8971 5.03616 9.99723C5.68682 8.97938 7.32552 6.89549 9.86094 6.83081C12.5809 6.76168 14.3724 9.10304 14.9635 10.0029C14.3135 11.0208 12.6741 13.1047 10.1393 13.1694ZM16.2578 9.68458C15.8532 8.97938 13.6184 5.44451 9.8286 5.5631C6.3229 5.65188 4.28403 8.7403 3.74245 9.68458C3.6302 9.8799 3.6302 10.1203 3.74245 10.3156C4.14134 11.0113 6.29753 14.439 10.0157 14.439C10.0677 14.439 10.1197 14.4383 10.1717 14.4371C13.6768 14.3476 15.7163 11.2599 16.2578 10.3156C16.3694 10.1203 16.3694 9.8799 16.2578 9.68458Z" fill="#BCCCD9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 10.9513C9.47555 10.9513 9.04876 10.5245 9.04876 10C9.04876 9.47556 9.47555 9.04876 10 9.04876C10.5245 9.04876 10.9513 9.47556 10.9513 10C10.9513 10.5245 10.5245 10.9513 10 10.9513ZM10 7.78042C8.77606 7.78042 7.78041 8.77607 7.78041 10C7.78041 11.224 8.77606 12.2196 10 12.2196C11.224 12.2196 12.2196 11.224 12.2196 10C12.2196 8.77607 11.224 7.78042 10 7.78042ZM10.1393 13.1694C7.4086 13.2328 5.62721 10.8971 5.03616 9.99723C5.68682 8.97938 7.32552 6.89549 9.86094 6.83081C12.5809 6.76168 14.3724 9.10304 14.9635 10.0029C14.3135 11.0208 12.6741 13.1047 10.1393 13.1694ZM16.2578 9.68458C15.8532 8.97938 13.6184 5.44451 9.8286 5.5631C6.3229 5.65188 4.28403 8.7403 3.74245 9.68458C3.6302 9.8799 3.6302 10.1203 3.74245 10.3156C4.14134 11.0113 6.29753 14.439 10.0157 14.439C10.0677 14.439 10.1197 14.4383 10.1717 14.4371C13.6768 14.3476 15.7163 11.2599 16.2578 10.3156C16.3694 10.1203 16.3694 9.8799 16.2578 9.68458Z" fill="white"/>
|
||||
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 6.001C10 6.3 9.695 6.515 9.695 6.515L1.134 11.818C0.51 12.227 0 11.924 0 11.149V0.852C0 0.0749997 0.51 -0.226 1.135 0.182L9.696 5.487C9.695 5.487 10 5.702 10 6.001Z" fill="#A3B3BF"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 299 B |
|
|
@ -0,0 +1,66 @@
|
|||
import React, { ReactNode, useState, useEffect } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { submit } from "redux-form";
|
||||
import { Dialog, Classes, Button, Intent, Callout } from "@blueprintjs/core";
|
||||
|
||||
type FormDialogComponentProps = {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
form: ReactNode;
|
||||
isSubmitting: boolean;
|
||||
submitIntent: string;
|
||||
formName: string;
|
||||
error?: string;
|
||||
dispatch: Function;
|
||||
};
|
||||
|
||||
export const FormDialogComponent = (props: FormDialogComponentProps) => {
|
||||
const submitHandler = () => props.dispatch(submit(props.formName));
|
||||
const [isPristine, makePristine] = useState(true);
|
||||
|
||||
const clearErrors = () => {
|
||||
makePristine(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.error) {
|
||||
makePristine(false);
|
||||
}
|
||||
}, [props.error]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={props.isOpen}
|
||||
canOutsideClickClose={false}
|
||||
canEscapeKeyClose={false}
|
||||
title={props.title}
|
||||
onClose={props.onClose}
|
||||
onOpening={clearErrors}
|
||||
>
|
||||
{props.error && !isPristine && (
|
||||
<Callout intent="danger">{props.error}</Callout>
|
||||
)}
|
||||
<div className={Classes.DIALOG_BODY}>{props.form}</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button
|
||||
text="Cancel"
|
||||
type="button"
|
||||
intent={Intent.NONE}
|
||||
onClick={props.onClose}
|
||||
/>
|
||||
<Button
|
||||
text="Submit"
|
||||
type="submit"
|
||||
onClick={submitHandler}
|
||||
intent={props.submitIntent as Intent}
|
||||
loading={props.isSubmitting && !props.error}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect()(FormDialogComponent);
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import React, { ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
import { ItemRenderer, Select } from "@blueprintjs/select";
|
||||
import { Button, MenuItem } from "@blueprintjs/core";
|
||||
import { DropdownOption } from "widgets/DropdownWidget";
|
||||
import { ControlIconName, ControlIcons } from "icons/ControlIcons";
|
||||
import { noop } from "utils/AppsmithUtils";
|
||||
import { Intent } from "constants/DefaultTheme";
|
||||
|
||||
export type ContextDropdownOption = DropdownOption & {
|
||||
onSelect: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
intent?: Intent;
|
||||
};
|
||||
const Dropdown = Select.ofType<ContextDropdownOption>();
|
||||
|
||||
const StyledMenuItem = styled(MenuItem)`
|
||||
&&&&.bp3-menu-item:hover {
|
||||
background: ${props => props.theme.colors.primary};
|
||||
color: ${props => props.theme.colors.textOnDarkBG};
|
||||
}
|
||||
&&&.bp3-menu-item.bp3-intent-danger:hover {
|
||||
background: ${props => props.theme.colors.error};
|
||||
}
|
||||
`;
|
||||
|
||||
type ContextDropdownProps = {
|
||||
options: ContextDropdownOption[];
|
||||
className: string;
|
||||
toggle: {
|
||||
type: "icon" | "button";
|
||||
icon?: ControlIconName;
|
||||
text?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const ContextDropdown = (props: ContextDropdownProps) => {
|
||||
let trigger: ReactNode;
|
||||
if (props.toggle.type === "icon" && props.toggle.icon)
|
||||
trigger = ControlIcons[props.toggle.icon]();
|
||||
if (props.toggle.type === "button" && props.toggle.text)
|
||||
trigger = <Button text={props.toggle.text} />;
|
||||
|
||||
const renderer: ItemRenderer<ContextDropdownOption> = (
|
||||
option: ContextDropdownOption,
|
||||
) => {
|
||||
return (
|
||||
<StyledMenuItem
|
||||
key={option.value}
|
||||
onClick={option.onSelect}
|
||||
shouldDismissPopover={true}
|
||||
text={option.label || option.value}
|
||||
intent={option.intent as Intent}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
hoverCloseDelay: 0,
|
||||
hoverOpenDelay: 0,
|
||||
modifiers: {
|
||||
arrow: {
|
||||
enabled: false,
|
||||
},
|
||||
offset: {
|
||||
enabled: true,
|
||||
offset: "-16px 0",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Dropdown
|
||||
items={props.options}
|
||||
itemRenderer={renderer}
|
||||
onItemSelect={noop}
|
||||
filterable={false}
|
||||
className={props.className}
|
||||
>
|
||||
{trigger}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContextDropdown;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component } from "react";
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
import { MenuItem, Menu, ControlGroup, InputGroup } from "@blueprintjs/core";
|
||||
import { BaseButton } from "../designSystems/blueprint/ButtonComponent";
|
||||
|
|
@ -128,11 +128,13 @@ class DropdownComponent extends Component<DropdownComponentProps> {
|
|||
activeItem={this.props.selected}
|
||||
noResults={<MenuItem disabled={true} text="No results." />}
|
||||
>
|
||||
<BaseButton
|
||||
styleName="secondary"
|
||||
text={this.getSelectedDisplayText()}
|
||||
rightIcon="chevron-down"
|
||||
/>
|
||||
{this.props.toggle || (
|
||||
<BaseButton
|
||||
styleName="secondary"
|
||||
text={this.getSelectedDisplayText()}
|
||||
rightIcon="chevron-down"
|
||||
/>
|
||||
)}
|
||||
</StyledDropdown>
|
||||
);
|
||||
}
|
||||
|
|
@ -150,6 +152,7 @@ export interface DropdownComponentProps {
|
|||
displayText: string;
|
||||
addItemHandler: (name: string) => void;
|
||||
};
|
||||
toggle?: ReactNode;
|
||||
}
|
||||
|
||||
export default DropdownComponent;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const Colors: Record<string, string> = {
|
|||
POLAR: "#E9FAF3",
|
||||
GEYSER: "#D3DEE3",
|
||||
GEYSER_LIGHT: "#D0D7DD",
|
||||
ATHENS_GRAY: "#FAFBFC",
|
||||
ATHENS_GRAY: "#EBEFF2",
|
||||
CONCRETE: "#F3F3F3",
|
||||
MYSTIC: "#E1E8ED",
|
||||
AQUA_HAZE: "#EEF2F5",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ const {
|
|||
ThemeProvider,
|
||||
} = styledComponents as styledComponents.ThemedStyledComponentsModule<Theme>;
|
||||
|
||||
export type Intent = "primary" | "danger" | "warning" | "none";
|
||||
|
||||
export type ThemeBorder = {
|
||||
thickness: number;
|
||||
style: "dashed" | "solid";
|
||||
|
|
@ -49,6 +51,10 @@ export type Theme = {
|
|||
card: {
|
||||
minWidth: number;
|
||||
minHeight: number;
|
||||
titleHeight: number;
|
||||
divider: ThemeBorder;
|
||||
hoverBG: Color;
|
||||
hoverBGOpacity: number;
|
||||
};
|
||||
shadows: string[];
|
||||
widgets: {
|
||||
|
|
@ -112,6 +118,8 @@ export const theme: Theme = {
|
|||
containerBorder: Colors.FRENCH_PASS,
|
||||
menuButtonBGInactive: Colors.JUNGLE_MIST,
|
||||
menuIconColorInactive: Colors.OXFORD_BLUE,
|
||||
bodyBG: Colors.ATHENS_GRAY,
|
||||
builderBodyBG: Colors.WHITE,
|
||||
},
|
||||
lineHeights: [0, 14, 18, 22, 24, 28, 36, 48, 64, 80],
|
||||
fonts: [
|
||||
|
|
@ -147,8 +155,16 @@ export const theme: Theme = {
|
|||
navItemHeight: 42,
|
||||
},
|
||||
card: {
|
||||
minWidth: 300,
|
||||
minHeight: 300,
|
||||
minWidth: 282,
|
||||
minHeight: 220,
|
||||
titleHeight: 48,
|
||||
divider: {
|
||||
thickness: 1,
|
||||
style: "solid",
|
||||
color: Colors.GEYSER_LIGHT,
|
||||
},
|
||||
hoverBG: Colors.BLACK,
|
||||
hoverBGOpacity: 0.5,
|
||||
},
|
||||
shadows: ["0px 2px 4px rgba(67, 70, 74, 0.14)"],
|
||||
widgets: {
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
export const API_EDITOR_FORM_NAME = "ApiEditorForm";
|
||||
export const CREATE_APPLICATION_FORM_NAME = "CreateApplicationForm";
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
export const ERROR_MESSAGE_SELECT_ACTION = "Please select an action";
|
||||
export const ERROR_MESSAGE_SELECT_ACTION_TYPE = "Please select an action type";
|
||||
|
||||
export const ERROR_MESSAGE_CREATE_APPLICATION =
|
||||
"We could not create the Application";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { IconProps, IconWrapper } from "../constants/IconConstants";
|
|||
import { ReactComponent as DeleteIcon } from "../assets/icons/control/delete.svg";
|
||||
import { ReactComponent as MoveIcon } from "../assets/icons/control/move.svg";
|
||||
import { ReactComponent as EditIcon } from "../assets/icons/control/edit.svg";
|
||||
import { ReactComponent as ViewIcon } from "../assets/icons/control/view.svg";
|
||||
import { ReactComponent as ViewIcon } from "assets/icons/control/view.svg";
|
||||
import { ReactComponent as MoreVerticalIcon } from "assets/icons/control/more-vertical.svg";
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
|
||||
|
|
@ -30,4 +31,11 @@ export const ControlIcons: {
|
|||
<ViewIcon />
|
||||
</IconWrapper>
|
||||
),
|
||||
MORE_VERTICAL_CONTROL: (props: IconProps) => (
|
||||
<IconWrapper {...props}>
|
||||
<MoreVerticalIcon />
|
||||
</IconWrapper>
|
||||
),
|
||||
};
|
||||
|
||||
export type ControlIconName = keyof typeof ControlIcons;
|
||||
|
|
|
|||
|
|
@ -17,3 +17,12 @@ code {
|
|||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
|
||||
div.bp3-select-popover .bp3-popover-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.bp3-popover-arrow {
|
||||
display:none;
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import HTML5Backend from "react-dnd-html5-backend";
|
|||
import { appInitializer } from "./utils/AppsmithUtils";
|
||||
import ProtectedRoute from "./pages/common/ProtectedRoute";
|
||||
import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
|
||||
|
||||
import {
|
||||
BASE_URL,
|
||||
BUILDER_URL,
|
||||
|
|
@ -31,12 +32,14 @@ import {
|
|||
import Applications from "./pages/Applications";
|
||||
|
||||
appInitializer();
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const store = createStore(
|
||||
appReducer,
|
||||
composeWithDevTools(applyMiddleware(sagaMiddleware)),
|
||||
);
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
ReactDOM.render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Provider store={store}>
|
||||
|
|
|
|||
13
app/client/src/mockComponentProps/ApplicationPayloads.tsx
Normal file
13
app/client/src/mockComponentProps/ApplicationPayloads.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { ApplicationPayload } from "constants/ReduxActionConstants";
|
||||
import faker from "faker";
|
||||
|
||||
export const getApplicationPayload = (): ApplicationPayload => ({
|
||||
id: faker.random.uuid(),
|
||||
name: faker.random.word(),
|
||||
organizationId: faker.random.uuid(),
|
||||
pageCount: faker.random.number(),
|
||||
});
|
||||
|
||||
export const getApplicationPayloads = (count: number): ApplicationPayload[] => {
|
||||
return [...Array(count).keys()].map(getApplicationPayload);
|
||||
};
|
||||
186
app/client/src/pages/Applications/ApplicationCard.tsx
Normal file
186
app/client/src/pages/Applications/ApplicationCard.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Card, Tooltip, Classes } from "@blueprintjs/core";
|
||||
import { ApplicationPayload } from "constants/ReduxActionConstants";
|
||||
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
|
||||
import {
|
||||
theme,
|
||||
getBorderCSSShorthand,
|
||||
getColorWithOpacity,
|
||||
} from "constants/DefaultTheme";
|
||||
import { ControlIcons } from "icons/ControlIcons";
|
||||
import ContextDropdown, {
|
||||
ContextDropdownOption,
|
||||
} from "components/editorComponents/ContextDropdown";
|
||||
|
||||
const Wrapper = styled(Card)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: ${props => props.theme.card.minWidth}px;
|
||||
height: ${props => props.theme.card.minHeight}px;
|
||||
position: relative;
|
||||
border-radius: ${props => props.theme.radii[1]}px;
|
||||
margin: ${props => props.theme.spaces[5]}px
|
||||
${props => props.theme.spaces[5]}px;
|
||||
&:hover {
|
||||
&:after {
|
||||
left: 0;
|
||||
top: 0;
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
& .control {
|
||||
display: block;
|
||||
z-index: 1;
|
||||
}
|
||||
& div.image-container {
|
||||
background: ${props =>
|
||||
getColorWithOpacity(
|
||||
props.theme.card.hoverBG,
|
||||
props.theme.card.hoverBGOpacity,
|
||||
)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
const ApplicationTitle = styled.div`
|
||||
font-size: ${props => props.theme.fontSizes[2]}px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: ${props => props.theme.card.titleHeight}px;
|
||||
padding: ${props => props.theme.spaces[6]}px;
|
||||
width: 100%;
|
||||
border-top: ${props => getBorderCSSShorthand(props.theme.card.divider)};
|
||||
font-weight: ${props => props.theme.fontWeights[2]};
|
||||
font-size: ${props => props.theme.fontSizes[4]}px;
|
||||
& {
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
.control {
|
||||
display: none;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: ${props => props.theme.spaces[5] * 3}px;
|
||||
top: ${props => props.theme.spaces[5]}px;
|
||||
}
|
||||
.more {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: ${props => props.theme.spaces[2]}px;
|
||||
top: ${props => props.theme.spaces[4]}px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ApplicationImage = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: calc(100% - ${props => props.theme.card.titleHeight}px);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
& {
|
||||
.control {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Control = styled.button<{ fixed?: boolean }>`
|
||||
outline: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const APPLICATION_CONTROL_FONTSIZE_INDEX = 6;
|
||||
|
||||
const viewControlIcon = ControlIcons.VIEW_CONTROL({
|
||||
width: theme.fontSizes[APPLICATION_CONTROL_FONTSIZE_INDEX],
|
||||
height: theme.fontSizes[APPLICATION_CONTROL_FONTSIZE_INDEX],
|
||||
});
|
||||
|
||||
type ApplicationCardProps = {
|
||||
application: ApplicationPayload;
|
||||
loading: boolean;
|
||||
duplicate: (applicationId: string) => void;
|
||||
share: (applicationId: string) => void;
|
||||
delete: (applicationId: string) => void;
|
||||
};
|
||||
|
||||
export const ApplicationCard = (props: ApplicationCardProps) => {
|
||||
const duplicateApp = () => {
|
||||
props.duplicate(props.application.id);
|
||||
};
|
||||
const shareApp = () => {
|
||||
props.share(props.application.id);
|
||||
};
|
||||
const deleteApp = () => {
|
||||
props.delete(props.application.id);
|
||||
};
|
||||
const moreActionItems: ContextDropdownOption[] = [
|
||||
{
|
||||
id: "duplicate",
|
||||
value: "duplicate",
|
||||
onSelect: duplicateApp,
|
||||
label: "Duplicate",
|
||||
},
|
||||
{
|
||||
id: "share",
|
||||
value: "share",
|
||||
onSelect: shareApp,
|
||||
label: "Share",
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
value: "delete",
|
||||
onSelect: deleteApp,
|
||||
label: "Delete",
|
||||
intent: "danger",
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Wrapper key={props.application.id}>
|
||||
<ApplicationTitle
|
||||
className={props.loading ? Classes.SKELETON : undefined}
|
||||
>
|
||||
<span>{props.application.name}</span>
|
||||
<Control className="control">
|
||||
<Tooltip content="View Application" hoverOpenDelay={500}>
|
||||
{viewControlIcon}
|
||||
</Tooltip>
|
||||
</Control>
|
||||
<ContextDropdown
|
||||
options={moreActionItems}
|
||||
toggle={{ type: "icon", icon: "MORE_VERTICAL_CONTROL" }}
|
||||
className="more"
|
||||
/>
|
||||
</ApplicationTitle>
|
||||
<ApplicationImage className="image-container">
|
||||
<Control className="control">
|
||||
<Tooltip content="Edit Application" hoverOpenDelay={500}>
|
||||
<BaseButton
|
||||
icon="edit"
|
||||
text="Edit"
|
||||
styleName="primary"
|
||||
filled={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Control>
|
||||
</ApplicationImage>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationCard;
|
||||
|
|
@ -1,38 +1,32 @@
|
|||
import React, { useRef, MutableRefObject } from "react";
|
||||
import { BaseButton } from "../../components/designSystems/blueprint/ButtonComponent";
|
||||
import { ControlGroup, Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
import { Form, reduxForm, InjectedFormProps } from "redux-form";
|
||||
import { CREATE_APPLICATION_FORM_NAME } from "constants/forms";
|
||||
import {
|
||||
CreateApplicationFormValues,
|
||||
createApplicationFormSubmitHandler,
|
||||
} from "utils/formhelpers";
|
||||
import TextField from "components/editorComponents/fields/TextField";
|
||||
import { required } from "utils/validation/common";
|
||||
import { FormGroup } from "@blueprintjs/core";
|
||||
|
||||
type CreateApplicationFormProps = {
|
||||
onCreate: (name: string) => void;
|
||||
creating: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const CreateApplicationForm = (props: CreateApplicationFormProps) => {
|
||||
const inputRef: MutableRefObject<HTMLInputElement | null> = useRef(null);
|
||||
const handleCreate = () => {
|
||||
if (inputRef && inputRef.current) {
|
||||
props.onCreate(inputRef.current.value);
|
||||
} else {
|
||||
//TODO (abhinav): Add validation code.
|
||||
}
|
||||
};
|
||||
export const CreateApplicationForm = (
|
||||
props: InjectedFormProps<CreateApplicationFormValues>,
|
||||
) => {
|
||||
const { error, handleSubmit } = props;
|
||||
return (
|
||||
<ControlGroup fill vertical>
|
||||
<input
|
||||
type="text"
|
||||
className={Classes.INPUT}
|
||||
ref={inputRef}
|
||||
placeholder="Application Name"
|
||||
/>
|
||||
<BaseButton
|
||||
text="Create Application"
|
||||
onClick={handleCreate}
|
||||
styleName="secondary"
|
||||
loading={props.creating}
|
||||
></BaseButton>
|
||||
</ControlGroup>
|
||||
<Form onSubmit={handleSubmit(createApplicationFormSubmitHandler)}>
|
||||
<FormGroup intent={error ? "danger" : "none"} helperText={error}>
|
||||
<TextField
|
||||
name="applicationName"
|
||||
placeholderMessage="Name"
|
||||
validate={required}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateApplicationForm;
|
||||
export default reduxForm<CreateApplicationFormValues>({
|
||||
form: CREATE_APPLICATION_FORM_NAME,
|
||||
onSubmit: createApplicationFormSubmitHandler,
|
||||
})(CreateApplicationForm);
|
||||
|
|
|
|||
|
|
@ -2,172 +2,103 @@ import React, { Component } from "react";
|
|||
import styled from "styled-components";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router";
|
||||
import {
|
||||
getApplicationBuilderURL,
|
||||
getApplicationViewerURL,
|
||||
} from "../../constants/routes";
|
||||
import { AppState } from "../../reducers";
|
||||
import {
|
||||
getApplicationList,
|
||||
getIsFetchingApplications,
|
||||
getIsCreatingApplication,
|
||||
getCreateApplicationError,
|
||||
} from "../../selectors/applicationSelectors";
|
||||
import {
|
||||
ReduxActionTypes,
|
||||
ApplicationPayload,
|
||||
} from "../../constants/ReduxActionConstants";
|
||||
import { Card, Spinner, Tooltip } from "@blueprintjs/core";
|
||||
import { ControlIcons } from "../../icons/ControlIcons";
|
||||
import { theme } from "../../constants/DefaultTheme";
|
||||
import { Divider } from "@blueprintjs/core";
|
||||
import ApplicationsHeader from "./ApplicationsHeader";
|
||||
import SubHeader from "pages/common/SubHeader";
|
||||
import { getApplicationPayloads } from "mockComponentProps/ApplicationPayloads";
|
||||
import ApplicationCard from "./ApplicationCard";
|
||||
import CreateApplicationForm from "./CreateApplicationForm";
|
||||
import { CREATE_APPLICATION_FORM_NAME } from "constants/forms";
|
||||
import { noop } from "utils/AppsmithUtils";
|
||||
|
||||
const APPLICATION_CONTROL_FONTSIZE_INDEX = 7;
|
||||
|
||||
const ApplicationsBody = styled.section`
|
||||
const ApplicationsPageWrapper = styled.section`
|
||||
width: 100vw;
|
||||
`;
|
||||
const SectionDivider = styled(Divider)`
|
||||
margin: ${props => props.theme.spaces[11]}px auto;
|
||||
width: 100%;
|
||||
`;
|
||||
const ApplicationsBody = styled.div`
|
||||
width: 1224px;
|
||||
min-height: calc(100vh - ${props => props.theme.headerHeight});
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: white;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
margin: ${props => props.theme.spaces[12]}px auto;
|
||||
background: ${props => props.theme.colors.bodyBG};
|
||||
`;
|
||||
|
||||
const ApplicationCardsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
align-items: space-around;
|
||||
width: 80%;
|
||||
justify-content: flex-start;
|
||||
align-items: space-evenly;
|
||||
`;
|
||||
const ApplicationCard = styled(Card)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: ${props => props.theme.card.minWidth}px;
|
||||
min-height: ${props => props.theme.card.minHeight}px;
|
||||
position: relative;
|
||||
margin-bottom: ${props => props.theme.spaces[2]}px;
|
||||
margin-right: ${props => props.theme.spaces[2]}px;
|
||||
&:hover {
|
||||
& div.controls {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const ApplicationTitle = styled.h1`
|
||||
font-size: ${props => props.theme.fontSizes[7]}px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
`;
|
||||
const ApplicationControls = styled.div`
|
||||
display: none;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-around;
|
||||
`;
|
||||
|
||||
const Control = styled.button<{ fixed?: boolean }>`
|
||||
outline: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: ${props => (props.fixed ? "absolute" : "auto")};
|
||||
right: ${props => props.theme.spaces[2]}px;
|
||||
top: ${props => props.theme.spaces[2]}px;
|
||||
`;
|
||||
|
||||
const editControlIcon = ControlIcons.EDIT_CONTROL({
|
||||
width: theme.fontSizes[APPLICATION_CONTROL_FONTSIZE_INDEX],
|
||||
height: theme.fontSizes[APPLICATION_CONTROL_FONTSIZE_INDEX],
|
||||
});
|
||||
|
||||
const deleteControlIcon = ControlIcons.DELETE_CONTROL({
|
||||
width: theme.fontSizes[APPLICATION_CONTROL_FONTSIZE_INDEX],
|
||||
height: theme.fontSizes[APPLICATION_CONTROL_FONTSIZE_INDEX],
|
||||
background: theme.colors.error,
|
||||
});
|
||||
|
||||
const viewControlIcon = ControlIcons.VIEW_CONTROL({
|
||||
width: theme.fontSizes[APPLICATION_CONTROL_FONTSIZE_INDEX],
|
||||
height: theme.fontSizes[APPLICATION_CONTROL_FONTSIZE_INDEX],
|
||||
});
|
||||
|
||||
type ApplicationProps = {
|
||||
applicationList: ApplicationPayload[];
|
||||
fetchApplications: () => void;
|
||||
createApplication: (appName: string) => void;
|
||||
isCreatingApplication: boolean;
|
||||
isFetchingApplications: boolean;
|
||||
createApplicationError?: string;
|
||||
history: any;
|
||||
};
|
||||
|
||||
class Applications extends Component<ApplicationProps> {
|
||||
handleEditApplication = (applicationId: string) => () => {
|
||||
this.props.history.push(getApplicationBuilderURL(applicationId));
|
||||
};
|
||||
|
||||
handleViewApplication = (applicationId: string, pageId?: string) => () => {
|
||||
this.props.history.push(getApplicationViewerURL(applicationId, pageId));
|
||||
};
|
||||
|
||||
renderApplicationCard = (application: ApplicationPayload) => {
|
||||
return (
|
||||
<ApplicationCard interactive key={application.id}>
|
||||
<ApplicationTitle>{application.name}</ApplicationTitle>
|
||||
<ApplicationControls className="controls">
|
||||
<Control fixed>
|
||||
<Tooltip content="Delete Application" hoverOpenDelay={500}>
|
||||
{deleteControlIcon}
|
||||
</Tooltip>
|
||||
</Control>
|
||||
<Control
|
||||
onClick={this.handleViewApplication(
|
||||
application.id,
|
||||
application.defaultPageId,
|
||||
)}
|
||||
>
|
||||
<Tooltip content="View Application" hoverOpenDelay={500}>
|
||||
{viewControlIcon}
|
||||
</Tooltip>
|
||||
</Control>
|
||||
<Control onClick={this.handleEditApplication(application.id)}>
|
||||
<Tooltip content="Edit Application" hoverOpenDelay={500}>
|
||||
{editControlIcon}
|
||||
</Tooltip>
|
||||
</Control>
|
||||
</ApplicationControls>
|
||||
</ApplicationCard>
|
||||
);
|
||||
};
|
||||
componentDidMount() {
|
||||
this.props.fetchApplications();
|
||||
}
|
||||
public render() {
|
||||
const applicationList = this.props.isFetchingApplications
|
||||
? getApplicationPayloads(4)
|
||||
: this.props.applicationList;
|
||||
return (
|
||||
<div>
|
||||
<ApplicationsHeader
|
||||
add={{
|
||||
form: (
|
||||
<CreateApplicationForm
|
||||
onCreate={this.props.createApplication}
|
||||
creating={this.props.isCreatingApplication}
|
||||
/>
|
||||
),
|
||||
title: "Create Application",
|
||||
}}
|
||||
/>
|
||||
<ApplicationsPageWrapper>
|
||||
<ApplicationsHeader />
|
||||
<ApplicationsBody>
|
||||
{this.props.applicationList ? (
|
||||
<ApplicationCardsWrapper>
|
||||
{this.props.applicationList.map(this.renderApplicationCard)}
|
||||
</ApplicationCardsWrapper>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
<SubHeader
|
||||
add={{
|
||||
form: <CreateApplicationForm />,
|
||||
title: "Create New App",
|
||||
formName: CREATE_APPLICATION_FORM_NAME,
|
||||
formSubmitIntent: "primary",
|
||||
isAdding:
|
||||
this.props.isCreatingApplication ||
|
||||
!!this.props.createApplicationError,
|
||||
errorAdding: this.props.createApplicationError,
|
||||
}}
|
||||
search={{
|
||||
placeholder: "Search",
|
||||
}}
|
||||
/>
|
||||
<SectionDivider />
|
||||
<ApplicationCardsWrapper>
|
||||
{applicationList.map((application: ApplicationPayload) => (
|
||||
<ApplicationCard
|
||||
key={application.id}
|
||||
loading={this.props.isFetchingApplications}
|
||||
application={application}
|
||||
share={noop}
|
||||
duplicate={noop}
|
||||
delete={noop}
|
||||
/>
|
||||
))}
|
||||
</ApplicationCardsWrapper>
|
||||
</ApplicationsBody>
|
||||
</div>
|
||||
</ApplicationsPageWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -176,6 +107,7 @@ const mapStateToProps = (state: AppState) => ({
|
|||
applicationList: getApplicationList(state),
|
||||
isFetchingApplications: getIsFetchingApplications(state),
|
||||
isCreatingApplication: getIsCreatingApplication(state),
|
||||
createApplicationError: getCreateApplicationError(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
|
|
@ -192,8 +124,5 @@ const mapDispatchToProps = (dispatch: any) => ({
|
|||
});
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Applications),
|
||||
connect(mapStateToProps, mapDispatchToProps)(Applications),
|
||||
);
|
||||
|
|
|
|||
92
app/client/src/pages/common/SubHeader.tsx
Normal file
92
app/client/src/pages/common/SubHeader.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import React, { ReactNode, useState, useEffect } from "react";
|
||||
import FormDialogComponent from "components/designSystems/blueprint/FormDialogComponent";
|
||||
import {
|
||||
ControlGroup,
|
||||
InputGroup,
|
||||
Button,
|
||||
IIconProps,
|
||||
} from "@blueprintjs/core";
|
||||
import styled from "styled-components";
|
||||
|
||||
const SubHeaderWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 ${props => props.theme.spaces[5]}px;
|
||||
`;
|
||||
const StyledAddButton = styled(Button)<IIconProps>`
|
||||
&&& {
|
||||
background: ${props => props.theme.colors.primary};
|
||||
span {
|
||||
color: white;
|
||||
}
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
const SearchContainer = styled.div``;
|
||||
|
||||
type ApplicationsSubHeaderProps = {
|
||||
add?: {
|
||||
form: ReactNode;
|
||||
title: string;
|
||||
formName: string;
|
||||
isAdding: boolean;
|
||||
formSubmitIntent: string;
|
||||
errorAdding?: string;
|
||||
};
|
||||
search?: {
|
||||
placeholder: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const ApplicationsSubHeader = (props: ApplicationsSubHeaderProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const openForm = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
const closeForm = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (props.add && !props.add.isAdding) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [props.add]);
|
||||
return (
|
||||
<SubHeaderWrapper>
|
||||
<SearchContainer>
|
||||
{props.search && (
|
||||
<ControlGroup>
|
||||
<InputGroup
|
||||
placeholder={props.search.placeholder}
|
||||
leftIcon="search"
|
||||
/>
|
||||
</ControlGroup>
|
||||
)}
|
||||
</SearchContainer>
|
||||
{props.add && (
|
||||
<StyledAddButton
|
||||
text={props.add.title}
|
||||
icon="plus"
|
||||
title={props.add.title}
|
||||
onClick={openForm}
|
||||
minimal
|
||||
/>
|
||||
)}
|
||||
{props.add && (
|
||||
<FormDialogComponent
|
||||
isOpen={isOpen}
|
||||
formName={props.add.formName}
|
||||
form={props.add.form}
|
||||
error={props.add.errorAdding}
|
||||
title={props.add.title}
|
||||
isSubmitting={props.add.isAdding}
|
||||
onClose={closeForm}
|
||||
submitIntent={props.add.formSubmitIntent}
|
||||
/>
|
||||
)}
|
||||
</SubHeaderWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationsSubHeader;
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
ReduxActionErrorTypes,
|
||||
ApplicationPayload,
|
||||
} from "../../constants/ReduxActionConstants";
|
||||
import { ERROR_MESSAGE_CREATE_APPLICATION } from "constants/messages";
|
||||
|
||||
const initialState: ApplicationsReduxState = {
|
||||
isFetchingApplications: false,
|
||||
|
|
@ -19,10 +20,18 @@ const applicationsReducer = createReducer(initialState, {
|
|||
[ReduxActionTypes.FETCH_APPLICATION_LIST_SUCCESS]: (
|
||||
state: ApplicationsReduxState,
|
||||
action: ReduxAction<{ applicationList: ApplicationPayload[] }>,
|
||||
) => ({ ...state, applicationList: action.payload }),
|
||||
) => ({
|
||||
...state,
|
||||
applicationList: action.payload,
|
||||
isFetchingApplications: false,
|
||||
}),
|
||||
[ReduxActionTypes.CREATE_APPLICATION_INIT]: (
|
||||
state: ApplicationsReduxState,
|
||||
) => ({ ...state, creatingApplication: true }),
|
||||
) => ({
|
||||
...state,
|
||||
creatingApplication: true,
|
||||
createApplicationError: undefined,
|
||||
}),
|
||||
[ReduxActionTypes.CREATE_APPLICATION_SUCCESS]: (
|
||||
state: ApplicationsReduxState,
|
||||
action: ReduxAction<ApplicationPayload>,
|
||||
|
|
@ -39,6 +48,7 @@ const applicationsReducer = createReducer(initialState, {
|
|||
return {
|
||||
...state,
|
||||
creatingApplication: false,
|
||||
createApplicationError: ERROR_MESSAGE_CREATE_APPLICATION,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -47,6 +57,7 @@ export interface ApplicationsReduxState {
|
|||
applicationList: ApplicationPayload[];
|
||||
isFetchingApplications: boolean;
|
||||
creatingApplication: boolean;
|
||||
createApplicationError?: string;
|
||||
}
|
||||
|
||||
export default applicationsReducer;
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import ApplicationApi, {
|
|||
CreateApplicationResponse,
|
||||
ApplicationPagePayload,
|
||||
} from "../api/ApplicationApi";
|
||||
import { call, put, takeLatest, all } from "redux-saga/effects";
|
||||
import { call, put, takeLatest, all, select } from "redux-saga/effects";
|
||||
|
||||
import { validateResponse } from "./ErrorSagas";
|
||||
import { getApplicationList } from "selectors/applicationSelectors";
|
||||
|
||||
export function* publishApplicationSaga(
|
||||
requestAction: ReduxAction<PublishApplicationRequest>,
|
||||
|
|
@ -86,32 +87,62 @@ export function* fetchApplicationListSaga() {
|
|||
}
|
||||
|
||||
export function* createApplicationSaga(
|
||||
request: ReduxAction<CreateApplicationRequest>,
|
||||
action: ReduxAction<{
|
||||
applicationName: string;
|
||||
resolve: any;
|
||||
reject: any;
|
||||
}>,
|
||||
) {
|
||||
try {
|
||||
const response: CreateApplicationResponse = yield call(
|
||||
ApplicationApi.createApplication,
|
||||
request.payload,
|
||||
const { applicationName, resolve, reject } = action.payload;
|
||||
const applicationList: ApplicationPayload[] = yield select(
|
||||
getApplicationList,
|
||||
);
|
||||
const isValidResponse = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
const application: ApplicationPayload = {
|
||||
id: response.data.id,
|
||||
name: response.data.name,
|
||||
organizationId: response.data.organizationId,
|
||||
pageCount: response.data.pages ? response.data.pages.length : 0,
|
||||
defaultPageId: getDefaultPageId(response.data.pages),
|
||||
};
|
||||
yield put({
|
||||
type: ReduxActionTypes.CREATE_APPLICATION_SUCCESS,
|
||||
payload: application,
|
||||
const existingApplication = applicationList.find(application => {
|
||||
return application.name === applicationName;
|
||||
});
|
||||
|
||||
if (existingApplication) {
|
||||
yield call(reject, {
|
||||
_error: "An application with this name already exists",
|
||||
});
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.CREATE_APPLICATION_ERROR,
|
||||
payload: {
|
||||
error: "Could not create application",
|
||||
show: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const request: CreateApplicationRequest = { name: applicationName };
|
||||
const response: CreateApplicationResponse = yield call(
|
||||
ApplicationApi.createApplication,
|
||||
request,
|
||||
);
|
||||
const isValidResponse = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
const application: ApplicationPayload = {
|
||||
id: response.data.id,
|
||||
name: response.data.name,
|
||||
organizationId: response.data.organizationId,
|
||||
pageCount: response.data.pages ? response.data.pages.length : 0,
|
||||
defaultPageId: getDefaultPageId(response.data.pages),
|
||||
};
|
||||
yield put({
|
||||
type: ReduxActionTypes.CREATE_APPLICATION_SUCCESS,
|
||||
payload: application,
|
||||
});
|
||||
yield call(resolve);
|
||||
} else {
|
||||
yield call(reject, { _error: "Could not create application" });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.CREATE_APPLICATION_ERROR,
|
||||
payload: {
|
||||
error,
|
||||
show: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ ActionErrorDisplayMap = {
|
|||
};
|
||||
|
||||
export function* errorSaga(
|
||||
errorAction: ReduxAction<{ error: ErrorPayloadType }>,
|
||||
errorAction: ReduxAction<{ error: ErrorPayloadType; show?: boolean }>,
|
||||
) {
|
||||
// Just a pass through for now.
|
||||
// Add procedures to customize errors here
|
||||
|
|
@ -56,10 +56,10 @@ export function* errorSaga(
|
|||
// Show a toast when the error occurs
|
||||
const {
|
||||
type,
|
||||
payload: { error },
|
||||
payload: { error, show = true },
|
||||
} = errorAction;
|
||||
const message = ActionErrorDisplayMap[type](error);
|
||||
AppToaster.show({ message, intent: Intent.DANGER });
|
||||
if (show) AppToaster.show({ message, intent: Intent.DANGER });
|
||||
yield put({
|
||||
type: ReduxActionTypes.REPORT_ERROR,
|
||||
payload: {
|
||||
|
|
|
|||
|
|
@ -22,3 +22,9 @@ export const getIsCreatingApplication = createSelector(
|
|||
(applications: ApplicationsReduxState): boolean =>
|
||||
applications.creatingApplication,
|
||||
);
|
||||
|
||||
export const getCreateApplicationError = createSelector(
|
||||
getApplicationsState,
|
||||
(applications: ApplicationsReduxState): string | undefined =>
|
||||
applications.createApplicationError,
|
||||
);
|
||||
|
|
|
|||
25
app/client/src/utils/formhelpers.ts
Normal file
25
app/client/src/utils/formhelpers.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { SubmissionError } from "redux-form";
|
||||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
|
||||
export type CreateApplicationFormValues = {
|
||||
applicationName: string;
|
||||
};
|
||||
|
||||
export const createApplicationFormSubmitHandler = (
|
||||
values: CreateApplicationFormValues,
|
||||
dispatch: any,
|
||||
): Promise<any> => {
|
||||
const { applicationName } = values;
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: ReduxActionTypes.CREATE_APPLICATION_INIT,
|
||||
payload: {
|
||||
resolve,
|
||||
reject,
|
||||
applicationName,
|
||||
},
|
||||
});
|
||||
}).catch(error => {
|
||||
throw new SubmissionError(error);
|
||||
});
|
||||
};
|
||||
|
|
@ -1555,6 +1555,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
|
||||
|
||||
"@types/faker@^4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/faker/-/faker-4.1.7.tgz#27e8dff9bc0d11c798e7c4a414c4dbd3755a9a34"
|
||||
integrity sha512-tq8puryvH3X1Stlg6mma27/BI2BOwcQOvg/uU7LH7dAsCnyfVEtUXTbcksMZgOA/BZur8rkn9C0CFEgpbxfTdA==
|
||||
|
||||
"@types/fontfaceobserver@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/fontfaceobserver/-/fontfaceobserver-0.0.6.tgz#14a4a02b77e66e6a1070622981d431c885a174ed"
|
||||
|
|
@ -5032,6 +5037,11 @@ extsprintf@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
|
||||
|
||||
faker@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f"
|
||||
integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=
|
||||
|
||||
fast-deep-equal@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user