diff --git a/app/client/.eslintrc.js b/app/client/.eslintrc.js index 4518ef96ce..431c668da0 100644 --- a/app/client/.eslintrc.js +++ b/app/client/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = { rules: { "@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/no-explicit-any": 0, + "react-hooks/rules-of-hooks": "error", }, settings: { react: { diff --git a/app/client/.storybook/config.js b/app/client/.storybook/config.js index 6f9aaffa6e..eebdba1cd1 100644 --- a/app/client/.storybook/config.js +++ b/app/client/.storybook/config.js @@ -1,3 +1,8 @@ +import "normalize.css/normalize.css"; +import "@blueprintjs/icons/lib/css/blueprint-icons.css"; +import "@blueprintjs/core/lib/css/blueprint.css"; +import "../src/index.css"; + import { configure, addDecorator } from "@storybook/react"; import { withContexts } from "@storybook/addon-contexts/react"; import { contexts } from "./configs/contexts"; diff --git a/app/client/package.json b/app/client/package.json index 3dca06d325..452a7f9ed0 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -44,18 +44,19 @@ "husky": "^3.0.5", "jsonpath-plus": "^1.0.0", "lint-staged": "^9.2.5", + "localforage": "^1.7.3", "lodash": "^4.17.11", "moment": "^2.24.0", "moment-timezone": "^0.5.27", - "monaco-editor": "^0.15.1", - "monaco-editor-webpack-plugin": "^1.7.0", + "monaco-editor": "^0.19.0", + "monaco-editor-webpack-plugin": "^1.1.0", "nanoid": "^2.0.4", "node-sass": "^4.11.0", "normalizr": "^3.3.0", "popper.js": "^1.15.0", "prettier": "^1.18.2", "re-reselect": "^3.4.0", - "react": "^16.7.0", + "react": "^16.12.0", "react-base-table": "^1.9.1", "react-dnd": "^9.3.4", "react-dnd-html5-backend": "^9.3.4", @@ -63,18 +64,18 @@ "react-dom": "^16.7.0", "react-helmet": "^5.2.1", "react-monaco-editor": "^0.31.1", - "react-redux": "^6.0.0", + "react-redux": "^7.1.3", "react-rnd": "^10.1.1", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", - "react-scripts": "^3.1.1", + "react-scripts": "^3.3.0", "react-select": "^3.0.8", - "react-transition-group": "^4.3.0", "react-simple-tree-menu": "^1.1.9", "react-tabs": "^3.0.0", + "react-transition-group": "^4.3.0", "redux": "^4.0.1", "redux-form": "^8.2.6", - "redux-saga": "^1.0.0", + "redux-saga": "^1.1.3", "reselect": "^4.0.0", "shallowequal": "^1.1.0", "source-map-explorer": "^2.1.1", @@ -129,6 +130,7 @@ "eslint-config-react": "^1.1.7", "eslint-plugin-prettier": "^3.1.0", "eslint-plugin-react": "^7.14.3", + "eslint-plugin-react-hooks": "^2.3.0", "react-docgen-typescript-loader": "^3.6.0", "react-is": "^16.12.0", "react-test-renderer": "^16.11.0", diff --git a/app/client/public/favicon-black.ico b/app/client/public/favicon-black.ico new file mode 100644 index 0000000000..340bfdd88f Binary files /dev/null and b/app/client/public/favicon-black.ico differ diff --git a/app/client/public/favicon-orange.ico b/app/client/public/favicon-orange.ico new file mode 100644 index 0000000000..47c59c8c3b Binary files /dev/null and b/app/client/public/favicon-orange.ico differ diff --git a/app/client/public/index.html b/app/client/public/index.html index ec593cba40..0edeec0ccd 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -10,11 +10,7 @@ - - - - - + Appsmith diff --git a/app/client/public/manifest.json b/app/client/public/manifest.json index 1f2f141faf..2d12e03717 100755 --- a/app/client/public/manifest.json +++ b/app/client/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "Appsmith App", + "name": "Appsmith Client Web UI", "icons": [ { "src": "favicon.ico", diff --git a/app/client/src/App.test.js b/app/client/src/App.test.js deleted file mode 100755 index a754b201bf..0000000000 --- a/app/client/src/App.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx index a67424af96..98307cc975 100755 --- a/app/client/src/App.tsx +++ b/app/client/src/App.tsx @@ -1,23 +1,14 @@ -import React, { Component } from "react"; -import { Helmet } from "react-helmet"; +import React from "react"; +import { Redirect } from "react-router-dom"; +import { useSelector } from "store"; +import { APPLICATIONS_URL } from "constants/routes"; import "./App.css"; import "../node_modules/@blueprintjs/core/src/blueprint.scss"; -class App extends Component { - render() { - return ( -
- - Appsmith - - -
-

Coming Soon

-
-
- ); - } -} +export const App = () => { + const currentUser = useSelector(state => state.ui.users.current); + return currentUser ? : null; +}; export default App; diff --git a/app/client/src/api/OrgApi.ts b/app/client/src/api/OrgApi.ts new file mode 100644 index 0000000000..5cac5438c0 --- /dev/null +++ b/app/client/src/api/OrgApi.ts @@ -0,0 +1,44 @@ +import { AxiosPromise } from "axios"; +import Api from "./Api"; +import { ApiResponse } from "./ApiResponses"; +import { OrgRole, Org } from "constants/orgConstants"; + +export interface FetchOrgRolesResponse extends ApiResponse { + data: OrgRole[]; +} + +export interface FetchOrgsResponse extends ApiResponse { + data: Org[]; +} +export interface FetchOrgResponse extends ApiResponse { + data: Org; +} + +export interface FetchOrgRequest { + orgId: string; +} + +export interface SaveOrgRequest { + id: string; + name: string; + website: string; +} + +class OrgApi extends Api { + static rolesURL = "v1/groups"; + static orgsURL = "v1/organizations"; + static fetchRoles(): AxiosPromise { + return Api.get(OrgApi.rolesURL); + } + static fetchOrgs(): AxiosPromise { + return Api.get(OrgApi.orgsURL); + } + static fetchOrg(request: FetchOrgRequest): AxiosPromise { + return Api.get(OrgApi.orgsURL + "/" + request.orgId); + } + static saveOrg(request: SaveOrgRequest): AxiosPromise { + return Api.put(OrgApi.orgsURL + "/" + request.id, request); + } +} + +export default OrgApi; diff --git a/app/client/src/api/UserApi.tsx b/app/client/src/api/UserApi.tsx index 5cd9080f1d..a1e5cee86d 100644 --- a/app/client/src/api/UserApi.tsx +++ b/app/client/src/api/UserApi.tsx @@ -34,11 +34,28 @@ export interface ResetPasswordVerifyTokenRequest { token: string; } +export interface FetchUserResponse extends ApiResponse { + id: string; +} + +export interface FetchUserRequest { + id: string; +} + +export interface InviteUserRequest { + email: string; + groupIds: string[]; + status?: string; +} + class UserApi extends Api { + //TODO(abhinav): make a baseURL, to which the other paths are added. static createURL = "v1/users"; static forgotPasswordURL = "v1/users/forgotPassword"; static verifyResetPasswordTokenURL = "v1/users/verifyPasswordResetToken"; static resetPasswordURL = "v1/users/resetPassword"; + static fetchUserURL = "v1/users"; + static inviteUserURL = "v1/users/invite"; static createUser( request: CreateUserRequest, ): AxiosPromise { @@ -62,6 +79,15 @@ class UserApi extends Api { ): AxiosPromise { return Api.get(UserApi.verifyResetPasswordTokenURL, request); } + + static fetchUser(request: FetchUserRequest): AxiosPromise { + return Api.get(UserApi.fetchUserURL + "/" + request.id); + } + + static inviteUser(request: InviteUserRequest): AxiosPromise { + request.status = "INVITED"; + return Api.post(UserApi.inviteUserURL, request); + } } export default UserApi; diff --git a/app/client/src/assets/icons/menu/org.svg b/app/client/src/assets/icons/menu/org.svg new file mode 100644 index 0000000000..fcfd90d8e0 --- /dev/null +++ b/app/client/src/assets/icons/menu/org.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/components/designSystems/appsmith/Dropdown.tsx b/app/client/src/components/designSystems/appsmith/Dropdown.tsx index f2c17de06f..4fe8fea30b 100644 --- a/app/client/src/components/designSystems/appsmith/Dropdown.tsx +++ b/app/client/src/components/designSystems/appsmith/Dropdown.tsx @@ -1,5 +1,6 @@ import React from "react"; import Select from "react-select"; + import { WrappedFieldInputProps } from "redux-form"; import { theme } from "constants/DefaultTheme"; diff --git a/app/client/src/components/designSystems/appsmith/StyledHeader.tsx b/app/client/src/components/designSystems/appsmith/StyledHeader.tsx index d89cb3a6ce..d5216eb3e4 100644 --- a/app/client/src/components/designSystems/appsmith/StyledHeader.tsx +++ b/app/client/src/components/designSystems/appsmith/StyledHeader.tsx @@ -2,7 +2,7 @@ import styled from "styled-components"; export default styled.header` display: flex; - width: 100vw; + width: 100%; justify-content: space-around; align-items: center; height: ${props => props.theme.headerHeight}; diff --git a/app/client/src/components/editorComponents/Button.tsx b/app/client/src/components/editorComponents/Button.tsx new file mode 100644 index 0000000000..0f3126e080 --- /dev/null +++ b/app/client/src/components/editorComponents/Button.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { Intent, BlueprintButtonIntentsCSS } from "constants/DefaultTheme"; +import styled, { css } from "styled-components"; +import { + AnchorButton as BlueprintAnchorButton, + Button as BlueprintButton, + Intent as BlueprintIntent, + IconName, +} from "@blueprintjs/core"; +import { Direction, Directions } from "utils/helpers"; + +const outline = css` + &&&&&& { + border-width: 1px; + border-style: solid; + } +`; + +const buttonStyles = css<{ + outline?: string; + intent?: Intent; + filled?: string; +}>` + ${BlueprintButtonIntentsCSS} + &&&& { + padding: ${props => + props.filled || props.outline + ? props.theme.spaces[3] + "px " + props.theme.spaces[3] + "px" + : 0}; + background: ${props => + props.filled || props.outline ? "auto" : "transparent"}; + } + + ${props => (props.outline ? outline : "")} +`; +const StyledButton = styled(BlueprintButton)<{ + outline?: string; + intent?: Intent; + filled?: string; +}>` + ${buttonStyles} +`; +const StyledAnchorButton = styled(BlueprintAnchorButton)<{ + outline?: string; + intent?: Intent; + filled?: string; +}>` + ${buttonStyles} +`; + +export type ButtonProps = { + outline?: boolean; + filled?: boolean; + intent?: Intent; + text?: string; + onClick?: () => void; + href?: string; + icon?: string; + iconAlignment?: Direction; +}; + +export const Button = (props: ButtonProps) => { + const icon: IconName | undefined = + props.icon && + (props.iconAlignment === Directions.LEFT || + props.iconAlignment === undefined) + ? (props.icon as IconName) + : undefined; + const rightIcon: IconName | undefined = + props.icon && props.iconAlignment === Directions.RIGHT + ? (props.icon as IconName) + : undefined; + + const baseProps = { + text: props.text, + minimal: !props.filled, + outline: props.outline ? props.outline.toString() : undefined, + filled: props.filled ? props.filled.toString() : undefined, + intent: props.intent as BlueprintIntent, + large: true, + }; + if (props.href) { + return ( + + ); + } else + return ( + + ); +}; + +export default Button; diff --git a/app/client/src/components/editorComponents/FormGroup.tsx b/app/client/src/components/editorComponents/FormGroup.tsx deleted file mode 100644 index 913ab8b59e..0000000000 --- a/app/client/src/components/editorComponents/FormGroup.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import styled from "styled-components"; -import { FormGroup } from "@blueprintjs/core"; -const StyledFormGroup = styled(FormGroup)` - & { - width: 100%; - } -`; -export default StyledFormGroup; diff --git a/app/client/src/components/editorComponents/SelectComponent.tsx b/app/client/src/components/editorComponents/SelectComponent.tsx new file mode 100644 index 0000000000..2b93c58a0f --- /dev/null +++ b/app/client/src/components/editorComponents/SelectComponent.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import CustomizedDropdown, { + CustomizedDropdownProps, +} from "pages/common/CustomizedDropdown/index"; + +type SelectComponentProps = { + input: { + value?: string; + onChange?: (value: string) => void; + }; + options?: Array<{ id: string; name: string }>; + placeholder?: string; +}; + +export const SelectComponent = (props: SelectComponentProps) => { + const dropdownProps: CustomizedDropdownProps = { + sections: [ + { + isSticky: false, + options: + props.options && + props.options.map(option => ({ + content: option.name, + onSelect: () => + props.input.onChange && props.input.onChange(option.id), + shouldCloseDropdown: true, + })), + }, + ], + trigger: { + text: props.input.value + ? props.options && + props.options.filter(option => props.input.value === option.id)[0] + .name + : props.placeholder, + outline: true, + }, + openDirection: "down", + }; + return ; +}; + +export default SelectComponent; diff --git a/app/client/src/components/editorComponents/TagInputComponent.tsx b/app/client/src/components/editorComponents/TagInputComponent.tsx new file mode 100644 index 0000000000..535f737c78 --- /dev/null +++ b/app/client/src/components/editorComponents/TagInputComponent.tsx @@ -0,0 +1,106 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +import { TagInput } from "@blueprintjs/core"; +import { + Intent, + IntentColors, + getColorWithOpacity, +} from "constants/DefaultTheme"; +const TagInputWrapper = styled.div<{ intent?: Intent }>` + &&& { + .bp3-tag { + color: ${props => props.theme.colors.textDefault}; + font-size: ${props => props.theme.fontSizes[3]}px; + background: ${props => + props.intent + ? getColorWithOpacity(IntentColors[props.intent], 0.2) + : getColorWithOpacity(IntentColors.none, 0.2)}; + border: 1px solid + ${props => + props.intent ? IntentColors[props.intent] : IntentColors.none}; + } + } +`; +type TagInputProps = { + /** TagInput Placeholder */ + placeholder: string; + /** TagInput value and onChange handler */ + input: { + value?: string; + onChange?: (value: string) => void; + }; + /** TagInput type of individual entries (HTML input types) */ + type: string; + /** A delimiter which decides when to separate string into tags */ + separator?: string | RegExp | undefined; + /** Intent of the tags, which defines their color */ + intent?: Intent; + hasError?: boolean; +}; + +/** + * TagInputComponent + * Takes in a comma separated set of values (input.value prop) to display in tags + * On addition or removal of tags, passes the comman separated string to input.onChange prop + * @param props : TagInputProps + */ +const TagInputComponent = (props: TagInputProps) => { + const _values = + props.input.value && + props.input.value.length > 0 && + props.input.value.split(","); + + const [values, setValues] = useState(_values || []); + const [currentValue, setCurrentValue] = useState(""); + + const commitValues = (newValues: string[]) => { + setValues(newValues); + props.input.onChange && + props.input.onChange(newValues.filter(Boolean).join(",")); + }; + const onTagsChange = (values: React.ReactNode[]) => { + const _values = values as string[]; + commitValues(_values); + }; + + const onKeyDown = (e: any) => { + if (e.key === "," || e.key === "Enter" || e.key === " ") { + const newValues = [...values, e.target.value]; + commitValues(newValues); + setCurrentValue(""); + } else if (e.key === "Backspace") { + if (e.target.value.length === 0) { + const newValues = values.slice(0, -1); + commitValues(newValues); + } + } + }; + + const handleInputChange = (e: any) => { + if ([",", " ", "Enter"].indexOf(e.target.value) === -1) { + setCurrentValue(e.target.value); + } + }; + + return ( + + + + ); +}; + +export default TagInputComponent; diff --git a/app/client/src/components/editorComponents/form/FieldError.tsx b/app/client/src/components/editorComponents/form/FieldError.tsx new file mode 100644 index 0000000000..6405fc16bf --- /dev/null +++ b/app/client/src/components/editorComponents/form/FieldError.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import styled from "styled-components"; +import { IntentColors } from "constants/DefaultTheme"; +// Note: This component is only for the input fields which donot have the +// popover error tooltip. This is also only for Appsmith components +// Not to be used in widgets / canvas + +const StyledError = styled.span<{ show: boolean }>` + text-align: left; + color: ${IntentColors.danger}; + font-size: ${props => props.theme.fontSizes[2]}px; + opacity: ${props => (props.show ? 1 : 0)}; + padding-left: ${props => props.theme.fontSizes[3]}px; + position: relative; + margin-top: ${props => props.theme.spaces[1]}px; + &:before { + position: absolute; + content: "!"; + color: white; + background: ${IntentColors.danger}; + height: ${props => props.theme.fontSizes[2]}px; + width: ${props => props.theme.fontSizes[2]}px; + border-radius: 50%; + position: absolute; + content: "!"; + color: white; + border-radius: 50%; + left: 0; + top: 2px; + display: flex; + font-weight: ${props => props.theme.fontWeights[2]}; + font-size: ${props => props.theme.fontSizes[1]}px; + justify-content: center; + align-items: center; + } +`; + +type FormFieldErrorProps = { + error?: string; +}; + +export const FormFieldError = (props: FormFieldErrorProps) => { + return {props.error}; +}; + +export default FormFieldError; diff --git a/app/client/src/components/editorComponents/form/FormActionButton.tsx b/app/client/src/components/editorComponents/form/FormActionButton.tsx new file mode 100644 index 0000000000..c4b30095c2 --- /dev/null +++ b/app/client/src/components/editorComponents/form/FormActionButton.tsx @@ -0,0 +1,12 @@ +import styled from "styled-components"; +import { Button } from "@blueprintjs/core"; +import { Intent, BlueprintButtonIntentsCSS } from "constants/DefaultTheme"; + +type FormActionButtonProps = { + intent?: Intent; + large?: boolean; +}; + +export default styled(Button)` + ${BlueprintButtonIntentsCSS} +`; diff --git a/app/client/src/components/editorComponents/form/FormFooter.tsx b/app/client/src/components/editorComponents/form/FormFooter.tsx new file mode 100644 index 0000000000..adbc781be3 --- /dev/null +++ b/app/client/src/components/editorComponents/form/FormFooter.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import styled from "styled-components"; +import FormActionButton from "components/editorComponents/form/FormActionButton"; +import Divider from "components/editorComponents/Divider"; + +type FormFooterProps = { + onCancel?: () => void; + onSubmit?: () => void; + divider?: boolean; + submitting: boolean; + submitText?: string; + cancelText?: string; + submitOnEnter?: boolean; +}; + +const FooterActions = styled.div` + margin: 1em; + display: flex; + justify-content: flex-end; + align-items: center; + & > button { + margin-left: 1em; + } +`; + +const FormFooterContainer = styled.div` + padding: 1em 0; +`; + +export const FormFooter = (props: FormFooterProps) => { + return ( + + {props.divider && } + + {props.onCancel && ( + + )} + {props.onSubmit && ( + + )} + + + ); +}; + +export default FormFooter; diff --git a/app/client/src/components/editorComponents/form/FormGroup.tsx b/app/client/src/components/editorComponents/form/FormGroup.tsx new file mode 100644 index 0000000000..6c207f712c --- /dev/null +++ b/app/client/src/components/editorComponents/form/FormGroup.tsx @@ -0,0 +1,11 @@ +import styled from "styled-components"; +import { FormGroup } from "@blueprintjs/core"; +type FormGroupProps = { + fill?: boolean; +}; +const StyledFormGroup = styled(FormGroup)` + & { + width: ${props => (props.fill ? "100%" : "auto")}; + } +`; +export default StyledFormGroup; diff --git a/app/client/src/components/editorComponents/form/MessageTag.tsx b/app/client/src/components/editorComponents/form/FormMessage.tsx similarity index 88% rename from app/client/src/components/editorComponents/form/MessageTag.tsx rename to app/client/src/components/editorComponents/form/FormMessage.tsx index a204a08069..51635cbe25 100644 --- a/app/client/src/components/editorComponents/form/MessageTag.tsx +++ b/app/client/src/components/editorComponents/form/FormMessage.tsx @@ -6,7 +6,7 @@ import { AnchorButton, Button, } from "@blueprintjs/core"; -import { Intent, BlueprintIntentsCSS } from "constants/DefaultTheme"; +import { Intent, BlueprintButtonIntentsCSS } from "constants/DefaultTheme"; export type MessageAction = { url?: string; @@ -34,7 +34,7 @@ const ActionsContainer = styled.div` align-items: center; & .appsmith-message-action-button { border: none; - ${BlueprintIntentsCSS} + ${BlueprintButtonIntentsCSS} } `; @@ -63,14 +63,14 @@ const ActionButton = (props: MessageAction) => { return null; }; -export type MessageTagProps = { +export type FormMessageProps = { intent: Intent; message: string; title?: string; actions?: MessageAction[]; }; -export const MessageTag = (props: MessageTagProps) => { +export const FormMessage = (props: FormMessageProps) => { const actions = props.actions && props.actions.map(action => ); @@ -83,4 +83,4 @@ export const MessageTag = (props: MessageTagProps) => { ); }; -export default MessageTag; +export default FormMessage; diff --git a/app/client/src/components/editorComponents/form/fields/SelectField.tsx b/app/client/src/components/editorComponents/form/fields/SelectField.tsx new file mode 100644 index 0000000000..c1367f4665 --- /dev/null +++ b/app/client/src/components/editorComponents/form/fields/SelectField.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { + Field, + WrappedFieldMetaProps, + WrappedFieldInputProps, +} from "redux-form"; +import FormFieldError from "components/editorComponents/form/FieldError"; +import SelectComponent from "components/editorComponents/SelectComponent"; + +const renderComponent = ( + componentProps: SelectFieldProps & { + meta: Partial; + input: Partial; + }, +) => { + return ( + + + + + ); +}; + +type SelectFieldProps = { + name: string; + placeholder?: string; + options?: Array<{ id: string; name: string; value?: string }>; +}; + +export const SelectField = (props: SelectFieldProps) => { + return ( + + ); +}; + +export default SelectField; diff --git a/app/client/src/components/editorComponents/form/fields/TagListField.tsx b/app/client/src/components/editorComponents/form/fields/TagListField.tsx new file mode 100644 index 0000000000..106077e249 --- /dev/null +++ b/app/client/src/components/editorComponents/form/fields/TagListField.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { + Field, + WrappedFieldMetaProps, + WrappedFieldInputProps, +} from "redux-form"; +import TagInputComponent from "components/editorComponents/TagInputComponent"; +import { Intent } from "constants/DefaultTheme"; +import FormFieldError from "components/editorComponents/form/FieldError"; + +const renderComponent = ( + componentProps: TagListFieldProps & { + meta: Partial; + input: Partial; + }, +) => { + return ( + + + + + + ); +}; + +type TagListFieldProps = { + name: string; + placeholder: string; + type: string; + label: string; + intent: Intent; +}; + +const TagListField = (props: TagListFieldProps) => { + return ( + + + + ); +}; + +export default TagListField; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 3ea43db3cf..7b84abda74 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -1,6 +1,7 @@ import * as styledComponents from "styled-components"; import { Colors, Color } from "./Colors"; import * as FontFamilies from "./Fonts"; +import tinycolor from "tinycolor2"; import _ from "lodash"; export type FontFamily = typeof FontFamilies[keyof typeof FontFamilies]; @@ -16,7 +17,7 @@ const { export const IntentColors: Record = { primary: Colors.GREEN, success: Colors.PURPLE, - secondary: Colors.GEYSER_LIGHT, + secondary: Colors.BLACK_PEARL, danger: Colors.RED, none: Colors.GEYSER_LIGHT, warning: Colors.JAFFA, @@ -24,24 +25,99 @@ export const IntentColors: Record = { export type Intent = typeof IntentColors[keyof typeof IntentColors]; -export const BlueprintIntentsCSS = css` - &.bp3.minimal.bp3-button { - color: ${IntentColors.none}; - } - &.bp3.minimal.bp3-intent-primary { - color: ${IntentColors.primary}; - } - &.bp3.minimal.bp3-intent-secondary { +export const darken = (color: Color, intensity: number) => { + return new tinycolor(color).darken(intensity).toString(); +}; + +export const darkenHover = (color: Color) => { + return darken(color, 8); +}; + +export const darkenActive = (color: Color) => { + return darken(color, 16); +}; + +const getHoverAndActiveStyles = (color: Color, filled = true) => { + return css` + background: ${color}; + border-color: ${filled ? color : "auto"}; + color: ${filled ? Colors.WHITE : "auto"}; + &:hover { + background: ${darkenHover(color)}; + border-color: ${darkenHover(color)}; + box-shadow: none; + } + &:active { + background: ${darkenActive(color)}; + border-color: ${darkenActive(color)}; + box-shadow: none; + } + `; +}; + +export const BlueprintButtonIntentsCSS = css` + &&&.bp3-button { + box-shadow: none; + display: flex; + border-width: 1px; + border-style: solid; + outline: none; + min-width: 100px; color: ${IntentColors.secondary}; + border-color: ${IntentColors.none}; + & span.bp3-icon { + color: ${IntentColors.none}; + } + background: ${Colors.WHITE}; } - &.bp3.minimal.bp3-intent-danger { + &&&.bp3-button.bp3-intent-primary:not(.bp3-minimal) { + background: ${IntentColors.primary}; + ${getHoverAndActiveStyles(IntentColors.primary)} + } + &&&.bp3-button.bp3-intent-secondary:not(.bp3-minimal) { + background: ${IntentColors.secondary}; + ${getHoverAndActiveStyles(IntentColors.secondary)} + } + &&&.bp3-button.bp3-intent-danger:not(.bp3-minimal) { + background: ${IntentColors.danger}; + ${getHoverAndActiveStyles(IntentColors.danger)} + } + &&&.bp3-button.bp3-intent-success:not(.bp3-minimal) { + background: ${IntentColors.success}; + ${getHoverAndActiveStyles(IntentColors.success)} + } + &&&.bp3-button.bp3-intent-warning:not(.bp3-minimal) { + background: ${IntentColors.warning}; + ${getHoverAndActiveStyles(IntentColors.warning)} + } + + &&&.bp3-minimal.bp3-button { + color: ${IntentColors.secondary}; + border: none; + border-color: ${IntentColors.none}; + & span.bp3-icon { + color: ${IntentColors.none}; + } + } + &&&.bp3-minimal.bp3-intent-primary { + color: ${IntentColors.primary}; + border-color: ${IntentColors.primary}; + } + &&&.bp3-minimal.bp3-intent-secondary { + color: ${IntentColors.secondary}; + border-color: ${IntentColors.secondary}; + } + &&&.bp3-minimal.bp3-intent-danger { color: ${IntentColors.danger}; + border-color: ${IntentColors.danger}; } - &.bp3.minimal.bp3-intent-warning { + &&&.bp3-minimal.bp3-intent-warning { color: ${IntentColors.warning}; + border-color: ${IntentColors.warning}; } - &.bp3.minimal.bp3-intent-success { + &&&.bp3-minimal.bp3-intent-success { color: ${IntentColors.success}; + border-color: ${IntentColors.success}; } `; @@ -100,6 +176,7 @@ export type Theme = { selectHighlightColor: Color; }; }; + pageContentWidth: number; }; export const getColorWithOpacity = (color: Color, opacity: number) => { @@ -224,6 +301,7 @@ export const theme: Theme = { selectHighlightColor: Colors.GEYSER_LIGHT, }, }, + pageContentWidth: 1224, }; export { css, createGlobalStyle, keyframes, ThemeProvider }; diff --git a/app/client/src/constants/IconConstants.tsx b/app/client/src/constants/IconConstants.tsx index 7622d82f16..c15cadf3f6 100644 --- a/app/client/src/constants/IconConstants.tsx +++ b/app/client/src/constants/IconConstants.tsx @@ -12,6 +12,7 @@ export const IconWrapper = styled.div` &:focus { outline: none; } + display: inline-block; svg { width: ${props => props.width || props.theme.fontSizes[7]}px; height: ${props => props.height || props.theme.fontSizes[7]}px; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index d86d133339..b0aafadc62 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -97,11 +97,28 @@ export const ReduxActionTypes: { [key: string]: string } = { RESET_USER_PASSWORD_SUCCESS: "RESET_USER_PASSWORD_SUCCESS", FETCH_PLUGINS_REQUEST: "FETCH_PLUGINS_REQUEST", FETCH_PLUGINS_SUCCESS: "FETCH_PLUGINS_SUCCESS", + INVITE_USERS_TO_ORG_INIT: "INVITE_USERS_TO_ORG_INIT", + INVITE_USERS_TO_ORG_SUCCESS: "INVITE_USERS_TO_ORG_SUCCESS", FORGOT_PASSWORD_INIT: "FORGOT_PASSWORD_INIT", FORGOT_PASSWORD_SUCCESS: "FORGOT_PASSWORD_SUCCESS", RESET_PASSWORD_VERIFY_TOKEN_SUCCESS: "RESET_PASSWORD_VERIFY_TOKEN_SUCCESS", RESET_PASSWORD_VERIFY_TOKEN_INIT: "RESET_PASSWORD_VERIFY_TOKEN_INIT", EXECUTE_PAGE_LOAD_ACTIONS: "EXECUTE_PAGE_LOAD_ACTIONS", + SWITCH_ORGANIZATION_INIT: "SWITCH_ORGANIZATION_INIT", + SWITCH_ORGANIZATION_SUCCESS: "SWITCH_ORGANIZATION_SUCCESS", + LOGOUT_USER_INIT: "LOGOUT_USER_INIT", + LOGOUT_USER_SUCCESS: "LOGOUT_USER_SUCCESS", + FETCH_ORG_ROLES_INIT: "FETCH_ORG_ROLES_INIT", + FETCH_ORG_ROLES_SUCCESS: "FETCH_ORG_ROLES_SUCCESS", + FETCH_ORG_INIT: "FETCH_ORG_INIT", + FETCH_ORG_SUCCESS: "FETCH_ORG_SUCCESS", + FETCH_ORGS_SUCCESS: "FETCH_ORGS_SUCCES", + FETCH_ORGS_INIT: "FETCH_ORGS_INIT", + SAVE_ORG_INIT: "SAVE_ORG_INIT", + FETCH_USER_INIT: "FETCH_USER_INIT", + FETCH_USER_SUCCESS: "FETCH_USER_SUCCESS", + SET_CURRENT_USER_INIT: "SET_CURRENT_USER_INIT", + SET_CURRENT_USER_SUCCESS: "SET_CURRENT_USER_SUCCESS", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; @@ -141,8 +158,18 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { RESET_USER_PASSWORD_ERROR: "RESET_USER_PASSWORD_ERROR", SAVE_JS_EXECUTION_RECORD: "SAVE_JS_EXECUTION_RECORD", FETCH_PLUGINS_ERROR: "FETCH_PLUGINS_ERROR", + UPDATE_ORG_NAME_ERROR: "UPDATE_ORG_NAME_ERROR", + SWITCH_ORGANIZATION_ERROR: "SWITCH_ORGANIZATION_ERROR", FORGOT_PASSWORD_ERROR: "FORGOT_PASSWORD_ERROR", RESET_PASSWORD_VERIFY_TOKEN_ERROR: "RESET_PASSWORD_VERIFY_TOKEN_ERROR", + LOGOUT_USER_ERROR: "LOGOUT_USER_ERROR", + FETCH_ORG_ROLES_ERROR: "FETCH_ORG_ROLES_ERROR", + INVITE_USERS_TO_ORG_ERROR: "INVITE_USERS_TO_ORG_ERROR", + SAVE_ORG_ERROR: "SAVE_ORG_ERROR", + FETCH_ORG_ERROR: "FETCH_ORG_ERROR", + FETCH_ORGS_ERROR: "FETCH_ORGS_ERROR", + FETCH_USER_ERROR: "FETCH_USER_ERROR", + SET_CURRENT_USER_ERROR: "SET_CURRENT_USER_ERROR", }; export const ReduxFormActionTypes: { [key: string]: string } = { diff --git a/app/client/src/constants/forms.ts b/app/client/src/constants/forms.ts index a02d93e9cd..96a06535a8 100644 --- a/app/client/src/constants/forms.ts +++ b/app/client/src/constants/forms.ts @@ -1,5 +1,6 @@ export const API_EDITOR_FORM_NAME = "ApiEditorForm"; export const CREATE_APPLICATION_FORM_NAME = "CreateApplicationForm"; +export const INVITE_USERS_TO_ORG_FORM = "InviteUsersToOrgForm"; export const LOGIN_FORM_NAME = "LoginForm"; export const SIGNUP_FORM_NAME = "SignupForm"; export const FORGOT_PASSWORD_FORM_NAME = "ForgotPasswordForm"; diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index aa26113f10..d58a61bfcd 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -82,3 +82,20 @@ export const ERROR_401 = export const ERROR_403 = "Permission Denied. Please contact your admin to gain access."; export const WIDGET_TYPE_VALIDATION_ERROR = "Value does not match type"; + +export const INVITE_USERS_VALIDATION_EMAIL_LIST = + "Invalid Email address(es) found:"; +export const INVITE_USERS_VALIDATION_ROLE_EMPTY = "Please select a role"; + +export const INVITE_USERS_EMAIL_LIST_PLACEHOLDER = "Comma separated emails"; +export const INVITE_USERS_ROLE_SELECT_PLACEHOLDER = "Select Role"; +export const INVITE_USERS_ROLE_SELECT_LABEL = "Role"; +export const INVITE_USERS_EMAIL_LIST_LABEL = "User emails"; +export const INVITE_USERS_ADD_EMAIL_LIST_FIELD = "Add more"; +export const INVITE_USERS_SUBMIT_BUTTON_TEXT = "Invite Users"; +export const INVITE_USERS_SUBMIT_ERROR = + "We were unable to invite the users, please try again later"; +export const INVITE_USERS_SUBMIT_SUCCESS = + "The users have been invited successfully"; +export const INVITE_USERS_VALIDATION_EMAILS_EMPTY = + "Please enter the user emails"; diff --git a/app/client/src/constants/orgConstants.ts b/app/client/src/constants/orgConstants.ts new file mode 100644 index 0000000000..e3c19da361 --- /dev/null +++ b/app/client/src/constants/orgConstants.ts @@ -0,0 +1,10 @@ +export type OrgRole = { + id: string; + name: string; +}; + +export type Org = { + id: string; + name: string; + website?: string; +}; diff --git a/app/client/src/constants/routes.ts b/app/client/src/constants/routes.ts index 8d906d40cf..fde6c44f1d 100644 --- a/app/client/src/constants/routes.ts +++ b/app/client/src/constants/routes.ts @@ -1,5 +1,6 @@ import { MenuIcons } from "icons/MenuIcons"; export const BASE_URL = "/"; +export const ORG_URL = "/org"; export const APPLICATIONS_URL = `/applications`; export const BUILDER_URL = "/applications/:applicationId/pages/:pageId/edit"; export const USER_AUTH_URL = "/user"; @@ -72,3 +73,6 @@ export const FORGOT_PASSWORD_URL = `${USER_AUTH_URL}/forgotPassword`; export const RESET_PASSWORD_URL = `${USER_AUTH_URL}/resetPassword`; export const SIGN_UP_URL = `${USER_AUTH_URL}/signup`; export const AUTH_LOGIN_URL = `${USER_AUTH_URL}/login`; + +export const ORG_INVITE_USERS_PAGE_URL = `${ORG_URL}/invite`; +export const ORG_SETTINGS_PAGE_URL = `${ORG_URL}/settings`; diff --git a/app/client/src/constants/userConstants.ts b/app/client/src/constants/userConstants.ts new file mode 100644 index 0000000000..94db0d382c --- /dev/null +++ b/app/client/src/constants/userConstants.ts @@ -0,0 +1,6 @@ +export type User = { + id: string; + email: string; + currentOrganizationId: string; + organizationIds: string[]; +}; diff --git a/app/client/src/icons/MenuIcons.tsx b/app/client/src/icons/MenuIcons.tsx index 1f56f66637..7d964602d7 100644 --- a/app/client/src/icons/MenuIcons.tsx +++ b/app/client/src/icons/MenuIcons.tsx @@ -2,7 +2,7 @@ import React from "react"; import { IconProps, IconWrapper } from "constants/IconConstants"; import { ReactComponent as WidgetsIcon } from "assets/icons/menu/widgets.svg"; import { ReactComponent as ApisIcon } from "assets/icons/menu/api.svg"; - +import { ReactComponent as OrgIcon } from "assets/icons/menu/org.svg"; /* eslint-disable react/display-name */ export const MenuIcons: { @@ -18,4 +18,9 @@ export const MenuIcons: { ), + ORG_ICON: (props: IconProps) => ( + + + + ), }; diff --git a/app/client/src/index.css b/app/client/src/index.css index 26e815c276..021611dbb4 100755 --- a/app/client/src/index.css +++ b/app/client/src/index.css @@ -22,11 +22,18 @@ code { monospace; } +* { + outline: none !important; +} div.bp3-select-popover .bp3-popover-content { padding: 0; } +span.bp3-popover-target { + display: block; +} + div.bp3-popover-arrow { display:none; } diff --git a/app/client/src/index.tsx b/app/client/src/index.tsx index efbbdf7784..bffe824a58 100755 --- a/app/client/src/index.tsx +++ b/app/client/src/index.tsx @@ -5,24 +5,19 @@ import Loader from "pages/common/Loader"; import "./index.css"; import * as serviceWorker from "./serviceWorker"; import { Router, Route, Switch } from "react-router-dom"; -import { createStore, applyMiddleware } from "redux"; import history from "./utils/history"; -import appReducer from "./reducers"; import { ThemeProvider, theme } from "./constants/DefaultTheme"; -import createSagaMiddleware from "redux-saga"; -import { rootSaga } from "./sagas"; import { DndProvider } from "react-dnd"; -// import TouchBackend from "react-dnd-touch-backend"; 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 store from "./store"; import { BASE_URL, BUILDER_URL, APP_VIEW_URL, APPLICATIONS_URL, + ORG_URL, USER_AUTH_URL, } from "./constants/routes"; @@ -33,16 +28,10 @@ const Editor = lazy(() => import("./pages/Editor")); const Applications = lazy(() => import("./pages/Applications")); const PageNotFound = lazy(() => import("./pages/common/PageNotFound")); const AppViewer = lazy(() => import("./pages/AppViewer")); +const Organization = lazy(() => import("./pages/organization")); appInitializer(); -const sagaMiddleware = createSagaMiddleware(); -const store = createStore( - appReducer, - composeWithDevTools(applyMiddleware(sagaMiddleware)), -); -sagaMiddleware.run(rootSaga); - ReactDOM.render( @@ -50,15 +39,16 @@ ReactDOM.render( - + + - - + + diff --git a/app/client/src/pages/Applications/ApplicationsHeader.tsx b/app/client/src/pages/Applications/ApplicationsHeader.tsx deleted file mode 100644 index 03e2ed141d..0000000000 --- a/app/client/src/pages/Applications/ApplicationsHeader.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import StyledHeader from "components/designSystems/appsmith/StyledHeader"; -import { - Popover, - Button, - Position, - IIconProps, - PopoverInteractionKind, -} from "@blueprintjs/core"; - -const StyledAddButton = styled(Button)` - &&& { - background: ${props => props.theme.colors.primary}; - span { - color: white; - } - } -`; - -type ApplicationsHeaderProps = { - add?: { - form: JSX.Element; - title: string; - }; -}; - -export const ApplicationsHeader = (props: ApplicationsHeaderProps) => { - return ( - - {props.add && ( - - - {props.add.form} - - )} - - ); -}; - -export default ApplicationsHeader; diff --git a/app/client/src/pages/Applications/CreateApplicationForm.tsx b/app/client/src/pages/Applications/CreateApplicationForm.tsx index e776a214dd..4a939055cd 100644 --- a/app/client/src/pages/Applications/CreateApplicationForm.tsx +++ b/app/client/src/pages/Applications/CreateApplicationForm.tsx @@ -6,7 +6,7 @@ import { createApplicationFormSubmitHandler, } from "./helpers"; import TextField from "components/editorComponents/form/fields/TextField"; -import { FormGroup } from "@blueprintjs/core"; +import FormGroup from "components/editorComponents/form/FormGroup"; export const CreateApplicationForm = ( props: InjectedFormProps, diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx index a0afcbf5ce..41709f1352 100644 --- a/app/client/src/pages/Applications/index.tsx +++ b/app/client/src/pages/Applications/index.tsx @@ -1,5 +1,4 @@ import React, { Component } from "react"; -import { Helmet } from "react-helmet"; import styled from "styled-components"; import { connect } from "react-redux"; import { AppState } from "reducers"; @@ -13,33 +12,15 @@ import { ReduxActionTypes, ApplicationPayload, } from "constants/ReduxActionConstants"; -import { Divider } from "@blueprintjs/core"; -import ApplicationsHeader from "./ApplicationsHeader"; +import PageWrapper from "pages/common/PageWrapper"; import SubHeader from "pages/common/SubHeader"; +import PageSectionDivider from "pages/common/PageSectionDivider"; 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 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; - 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; @@ -65,47 +46,40 @@ class Applications extends Component { ? getApplicationPayloads(4) : this.props.applicationList; return ( - - - - Applications | Appsmith - - - - , - 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", - }} - /> - - - {applicationList.map((application: ApplicationPayload) => { - return ( - application.pageCount > 0 && ( - - ) - ); - })} - - - + + , + 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", + }} + /> + + + {applicationList.map((application: ApplicationPayload) => { + return ( + application.pageCount > 0 && ( + + ) + ); + })} + + ); } } diff --git a/app/client/src/pages/Editor/EditorHeader.tsx b/app/client/src/pages/Editor/EditorHeader.tsx index 56307bbae6..c6bb641ffb 100644 --- a/app/client/src/pages/Editor/EditorHeader.tsx +++ b/app/client/src/pages/Editor/EditorHeader.tsx @@ -35,13 +35,15 @@ const PreviewPublishSection = styled.div` `; const StretchedBreadCrumb = styled(Breadcrumbs)` - flex-shrink: 1; - * { - font-family: ${props => props.theme.fonts[0]}; - font-size: ${props => props.theme.fontSizes[2]}px; - } - li:after { - background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.71 7.29l-4-4a1.003 1.003 0 0 0-1.42 1.42L8.59 8 5.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l4-4c.18-.18.29-.43.29-.71 0-.28-.11-.53-.29-.71z' fill='rgba(92,112,128,1)'/%3E%3C/svg%3E"); + && { + flex-shrink: 1; + * { + font-family: ${props => props.theme.fonts[0]}; + font-size: ${props => props.theme.fontSizes[2]}px; + } + li:after { + background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.71 7.29l-4-4a1.003 1.003 0 0 0-1.42 1.42L8.59 8 5.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l4-4c.18-.18.29-.43.29-.71 0-.28-.11-.53-.29-.71z' fill='rgba(92,112,128,1)'/%3E%3C/svg%3E"); + } } `; diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index 39a43df537..67e418b2d1 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -103,6 +103,7 @@ class Editor extends Component { Editor | Appsmith + { return ( {submitSucceeded && ( - )} - {submitFailed && error && } + {submitFailed && error && }

{FORGOT_PASSWORD_PAGE_TITLE}

{FORGOT_PASSWORD_PAGE_SUBTITLE}
diff --git a/app/client/src/pages/UserAuth/Login.tsx b/app/client/src/pages/UserAuth/Login.tsx index 7c87cb2e26..cdfe1c5e8d 100644 --- a/app/client/src/pages/UserAuth/Login.tsx +++ b/app/client/src/pages/UserAuth/Login.tsx @@ -27,8 +27,8 @@ import { LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK, } from "constants/messages"; import Divider from "components/editorComponents/Divider"; -import MessageTag from "components/editorComponents/form/MessageTag"; -import FormGroup from "components/editorComponents/FormGroup"; +import FormMessage from "components/editorComponents/form/FormMessage"; +import FormGroup from "components/editorComponents/form/FormGroup"; import TextField from "components/editorComponents/form/fields/TextField"; import FormButton from "components/editorComponents/FormButton"; import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth"; @@ -85,7 +85,7 @@ export const Login = (props: LoginFormProps) => { return ( {showError && pristine && ( - { message = error; } - const messageTagProps: MessageTagProps = { + const messageTagProps: FormMessageProps = { intent: showInvalidMessage || showExpiredMessage || showFailureMessage ? "danger" @@ -136,7 +136,7 @@ export const ResetPassword = (props: ResetPasswordProps) => { }; if (showInvalidMessage || showExpiredMessage) { - return ; + return ; } if (!isTokenValid && validatingToken) { @@ -145,7 +145,7 @@ export const ResetPassword = (props: ResetPasswordProps) => { return ( {(showSuccessMessage || showFailureMessage) && ( - + )}

{RESET_PASSWORD_PAGE_TITLE}

diff --git a/app/client/src/pages/UserAuth/SignUp.tsx b/app/client/src/pages/UserAuth/SignUp.tsx index 4e6f7565a7..88e12ffa39 100644 --- a/app/client/src/pages/UserAuth/SignUp.tsx +++ b/app/client/src/pages/UserAuth/SignUp.tsx @@ -32,8 +32,8 @@ import { SIGNUP_PAGE_SUCCESS, SIGNUP_PAGE_SUCCESS_LOGIN_BUTTON_TEXT, } from "constants/messages"; -import MessageTag from "components/editorComponents/form/MessageTag"; -import FormGroup from "components/editorComponents/FormGroup"; +import FormMessage from "components/editorComponents/form/FormMessage"; +import FormGroup from "components/editorComponents/form/FormGroup"; import TextField from "components/editorComponents/form/fields/TextField"; import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth"; import FormButton from "components/editorComponents/FormButton"; @@ -69,7 +69,7 @@ export const SignUp = (props: InjectedFormProps) => { return ( {submitSucceeded && ( - ) => { ]} /> )} - {submitFailed && error && } + {submitFailed && error && }

{SIGNUP_PAGE_TITLE}

{SIGNUP_PAGE_SUBTITLE}
diff --git a/app/client/src/pages/common/CustomizedDropdown/Badge.tsx b/app/client/src/pages/common/CustomizedDropdown/Badge.tsx new file mode 100644 index 0000000000..69d37402a6 --- /dev/null +++ b/app/client/src/pages/common/CustomizedDropdown/Badge.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import styled from "styled-components"; +const BadgeWrapper = styled.div` + &&&&& { + display: flex; + flex-directition: row; + justify-content: flex-start; + img { + flex-basis: 0 1 auto; + width: 32px; + height: 32px; + border-radius: 50%; + margin-right: 16px; + } + div { + flex-basis: 1 0 auto; + flex-direction: column; + justify-content: space-around; + align-items: flex-start; + & h3, + h5 { + font-weight: ${props => props.theme.fontWeights[1]}; + margin: 0; + } + & h5 { + color: ${props => props.theme.colors.paneText}; + font-size: ${props => props.theme.fontSizes[3]}px; + } + } + } +`; + +type BadgeProps = { + imageURL?: string; + text: string; + subtext?: string; +}; + +export const Badge = (props: BadgeProps) => { + return ( + + {props.text} +
+

{props.text}

+
{props.subtext}
+
+
+ ); +}; + +export default Badge; diff --git a/app/client/src/pages/common/CustomizedDropdown/OrgDropdownData.tsx b/app/client/src/pages/common/CustomizedDropdown/OrgDropdownData.tsx new file mode 100644 index 0000000000..6e8e61c7b4 --- /dev/null +++ b/app/client/src/pages/common/CustomizedDropdown/OrgDropdownData.tsx @@ -0,0 +1,124 @@ +import React from "react"; +import Badge from "./Badge"; +import { Directions } from "utils/helpers"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { getOnSelectAction, DropdownOnSelectActions } from "./dropdownHelpers"; +import DropdownComponent, { CustomizedDropdownProps } from "./index"; +import { Link } from "react-router-dom"; +import { Org } from "constants/orgConstants"; +import { User } from "constants/userConstants"; + +const switchdropdown = (orgs: Org[]): CustomizedDropdownProps => ({ + sections: [ + { + isSticky: true, + options: [ + { + content: "Create Organization", + onSelect: () => getOnSelectAction(DropdownOnSelectActions.FORM, {}), + }, + ], + }, + { + options: orgs.map(org => ({ + content: org.name, + onSelect: () => + getOnSelectAction(DropdownOnSelectActions.DISPATCH, { + type: ReduxActionTypes.SWITCH_ORGANIZATION_INIT, + payload: { + organizationId: org.id, + }, + }), + })), + }, + ], + trigger: { + text: "Switch Organization", + }, + openDirection: Directions.RIGHT, + openOnHover: false, +}); + +export const options = ( + orgs: Org[], + currentOrg: Org, + user: User, +): CustomizedDropdownProps => ({ + sections: [ + { + options: [ + { + content: ( + + ), + active: false, + }, + { + content: Organization Settings, + }, + { + content: Members, + }, + { + content: Usage & Billing, + }, + { + content: Support, + }, + { + content: , + shouldCloseDropdown: false, + }, + { + content: "Switch To Personal Workspace", + onSelect: () => + getOnSelectAction(DropdownOnSelectActions.DISPATCH, { + type: ReduxActionTypes.SWITCH_ORGANIZATION_INIT, + payload: { organizationId: currentOrg.id }, + }), + }, + ], + }, + { + options: [ + { + content: ( + + ), + active: false, + }, + { + content: "Settings", + onSelect: () => + getOnSelectAction( + DropdownOnSelectActions.REDIRECT, + "/org/settings", + ), + }, + { + content: "Sign Out", + onSelect: () => + getOnSelectAction(DropdownOnSelectActions.DISPATCH, { + type: ReduxActionTypes.LOGOUT_USER_INIT, + }), + }, + ], + }, + ], + trigger: { + icon: "ORG_ICON", + text: currentOrg.name, + outline: false, + }, + openDirection: Directions.DOWN, +}); + +export default options; diff --git a/app/client/src/pages/common/CustomizedDropdown/StyledComponents.tsx b/app/client/src/pages/common/CustomizedDropdown/StyledComponents.tsx new file mode 100644 index 0000000000..4d9d237a3e --- /dev/null +++ b/app/client/src/pages/common/CustomizedDropdown/StyledComponents.tsx @@ -0,0 +1,87 @@ +import styled, { css } from "styled-components"; +import { Intent, IntentColors } from "constants/DefaultTheme"; + +export const DropdownTrigger = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + & > div > div { + margin-right: 20px; + } + &&& button { + width: 100%; + color: inherit; + justify-content: space-between; + outline: 0; + span { + color: inherit; + } + &:hover { + background: inherit; + } + } +`; +export const DropdownContent = styled.div` + &&& * { + font-size: ${props => props.theme.fontSizes[4]}px; + } +`; + +export const DropdownContentSection = styled.div<{ stick: boolean }>` + position: ${props => (props.stick ? "sticky" : "relative")}; + background: white; + z-index: ${props => (props.stick ? 1 : 0)}; + padding: 8px 0; + &&&&&& button { + padding: 0; + min-height: 0px; + } + &:first-of-type { + padding: 0 0 0px 0; + } + &:last-of-type { + padding: 0px 0 0 0; + } + &:not(:last-of-type):after { + position: absolute; + content: ""; + left: 0; + right: 0; + bottom: 0; + height: 1px; + background: #ccc; + } +`; + +export const onHover = css<{ intent?: Intent }>` + cursor: pointer; + &&&&:hover { + & * { + color: ${props => props.theme.colors.textOnDarkBG}; + text-decoration: none; + } + & a:hover { + text-decoration: none; + } + background: ${props => + props.intent ? IntentColors[props.intent] : IntentColors.primary}; + color: ${props => props.theme.colors.textOnDarkBG}; + } +`; + +export const Option = styled.div<{ intent?: Intent; active?: boolean }>` + padding: 8px 16px; + margin: 4px 0; + + & a { + color: ${props => props.theme.colors.textDefault}; + text-decoration: none; + } + &:first-of-type, + &:last-of-type { + margin: 0; + } + ${props => (props.active ? onHover : ``)}; +`; diff --git a/app/client/src/pages/common/CustomizedDropdown/dropdownHelpers.tsx b/app/client/src/pages/common/CustomizedDropdown/dropdownHelpers.tsx new file mode 100644 index 0000000000..a3722a7d31 --- /dev/null +++ b/app/client/src/pages/common/CustomizedDropdown/dropdownHelpers.tsx @@ -0,0 +1,58 @@ +import store from "store"; +import { IconNames } from "@blueprintjs/icons"; +import { Direction, Directions } from "utils/helpers"; +import { PopoverPosition } from "@blueprintjs/core"; + +export const DropdownOnSelectActions: { [id: string]: string } = { + REDIRECT: "redirect", + DISPATCH: "dispatch", +}; + +type DropdownOnSelectActionType = typeof DropdownOnSelectActions[keyof typeof DropdownOnSelectActions]; + +// TODO(abhinav): Figure out how to enforce payload type. +export const getOnSelectAction = ( + type: DropdownOnSelectActionType, + payload: any, +) => { + switch (type) { + case DropdownOnSelectActions.DISPATCH: + store.dispatch(payload); + break; + default: + console.log("No such action registered", type); + } +}; + +export const getDirectionBased: { + [id: string]: (direction: Direction) => string; +} = { + ICON_NAME: (direction: Direction) => { + switch (direction) { + case Directions.UP: + return IconNames.CHEVRON_UP; + case Directions.DOWN: + return IconNames.CHEVRON_DOWN; + case Directions.LEFT: + return IconNames.CHEVRON_LEFT; + case Directions.RIGHT: + return IconNames.CHEVRON_RIGHT; + default: + return IconNames.CHEVRON_DOWN; + } + }, + POPPER_POSITION: (direction: Direction) => { + switch (direction) { + case Directions.UP: + return PopoverPosition.TOP; + case Directions.DOWN: + return PopoverPosition.BOTTOM; + case Directions.LEFT: + return PopoverPosition.LEFT_BOTTOM; + case Directions.RIGHT: + return PopoverPosition.RIGHT_TOP; + default: + return PopoverPosition.BOTTOM_RIGHT; + } + }, +}; diff --git a/app/client/src/pages/common/CustomizedDropdown/index.tsx b/app/client/src/pages/common/CustomizedDropdown/index.tsx new file mode 100644 index 0000000000..3b27c77882 --- /dev/null +++ b/app/client/src/pages/common/CustomizedDropdown/index.tsx @@ -0,0 +1,118 @@ +import React, { ReactNode } from "react"; +import { withTheme } from "styled-components"; +import { + Popover, + IconName, + PopoverPosition, + Classes, + PopoverInteractionKind, +} from "@blueprintjs/core"; +import { MenuIcons } from "icons/MenuIcons"; +import { Intent } from "constants/DefaultTheme"; +import { Direction, Directions } from "utils/helpers"; +import { getDirectionBased } from "./dropdownHelpers"; +import { Theme } from "constants/DefaultTheme"; +import { + Option, + DropdownContentSection, + DropdownContent, + DropdownTrigger, +} from "./StyledComponents"; +import Button, { ButtonProps } from "components/editorComponents/Button"; + +export type CustomizedDropdownOptionSection = { + isSticky?: boolean; + options?: CustomizedDropdownOption[]; +}; + +export type CustomizedDropdownOption = { + content: ReactNode; + active?: boolean; + onSelect?: () => void; + intent?: Intent; + shouldCloseDropdown?: boolean; +}; + +export type CustomizedDropdownProps = { + sections: CustomizedDropdownOptionSection[]; + trigger: ButtonProps & { + content?: ReactNode; + }; + openDirection: Direction; + openOnHover?: boolean; +}; + +const getContentSection = (section: CustomizedDropdownOptionSection) => { + return ( + + {section.options && + section.options.map((option, index) => { + const shouldClose = + option.shouldCloseDropdown === undefined || + option.shouldCloseDropdown; + return ( + + ); + })} + + ); +}; + +export const CustomizedDropdown = ( + props: CustomizedDropdownProps & { theme: Theme }, +) => { + const icon = + props.trigger.icon && + MenuIcons[props.trigger.icon]({ + color: props.theme.colors.info, + width: 16, + height: 16, + }); + const trigger = ( + + {icon &&
{icon}
} + {props.trigger.content || ( + + + history.goBack()} + submitOnEnter={false} + submitText={INVITE_USERS_SUBMIT_BUTTON_TEXT} + > + + ); +}; + +export default connect( + (state: AppState) => { + return { + roles: getRoles(state), + }; + }, + (dispatch: any) => ({ + fetchRoles: () => dispatch({ type: ReduxActionTypes.FETCH_ORG_ROLES_INIT }), + }), +)( + reduxForm< + InviteUsersToOrgFormValues, + { fetchRoles: () => void; roles?: OrgRole[] } + >({ + form: INVITE_USERS_TO_ORG_FORM, + validate, + initialValues: { + usersByRole: [ + { + id: generateReactKey(), + }, + ], + }, + })(InviteUsersForm), +); diff --git a/app/client/src/pages/organization/create.tsx b/app/client/src/pages/organization/create.tsx new file mode 100644 index 0000000000..e711bd3bee --- /dev/null +++ b/app/client/src/pages/organization/create.tsx @@ -0,0 +1 @@ +export default "Create Organization"; diff --git a/app/client/src/pages/organization/defaultOrgPage.tsx b/app/client/src/pages/organization/defaultOrgPage.tsx new file mode 100644 index 0000000000..24fba504c8 --- /dev/null +++ b/app/client/src/pages/organization/defaultOrgPage.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import Centered from "components/designSystems/appsmith/CenteredWrapper"; +export const DefaultOrgPage = () => ( + +

This page is under construction

+
+); +export default DefaultOrgPage; diff --git a/app/client/src/pages/organization/helpers.ts b/app/client/src/pages/organization/helpers.ts new file mode 100644 index 0000000000..faf743bf5a --- /dev/null +++ b/app/client/src/pages/organization/helpers.ts @@ -0,0 +1,32 @@ +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { SubmissionError } from "redux-form"; +export type InviteUsersToOrgByRoleValues = { + id: string; + users?: string; + role?: string; +}; +export type InviteUsersToOrgFormValues = { + usersByRole: InviteUsersToOrgByRoleValues[]; +}; + +export const inviteUsersToOrgSubmitHandler = ( + values: InviteUsersToOrgFormValues, + dispatch: any, +): Promise => { + const data = values.usersByRole.map(value => ({ + roleId: value.role, + emails: value.users ? value.users.split(",") : [], + })); + return new Promise((resolve, reject) => { + dispatch({ + type: ReduxActionTypes.INVITE_USERS_TO_ORG_INIT, + payload: { + resolve, + reject, + data, + }, + }); + }).catch(error => { + throw new SubmissionError(error); + }); +}; diff --git a/app/client/src/pages/organization/index.tsx b/app/client/src/pages/organization/index.tsx new file mode 100644 index 0000000000..95819ba3e6 --- /dev/null +++ b/app/client/src/pages/organization/index.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Switch, Route, useRouteMatch, useLocation } from "react-router-dom"; +import { TransitionGroup, CSSTransition } from "react-transition-group"; +import PageWrapper from "pages/common/PageWrapper"; +import Users from "pages/users"; +import Settings from "./settings"; +import Invite from "./invite"; +import DefaultOrgPage from "./defaultOrgPage"; +export const Organization = () => { + const { path } = useRouteMatch(); + const location = useLocation(); + return ( + + + + + + + + + + + + + ); +}; + +export default Organization; diff --git a/app/client/src/pages/organization/invite.tsx b/app/client/src/pages/organization/invite.tsx new file mode 100644 index 0000000000..4523156e7d --- /dev/null +++ b/app/client/src/pages/organization/invite.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import InviteUsersForm from "./InviteUsersForm"; + +export const Invite = () => { + // const handleInviteUsersSubmit = (values: InviteUsersFormValues) => {}; + return ( + +

Invite Users

+ +
+ ); +}; + +export default Invite; diff --git a/app/client/src/pages/organization/settings.tsx b/app/client/src/pages/organization/settings.tsx new file mode 100644 index 0000000000..235efa9aeb --- /dev/null +++ b/app/client/src/pages/organization/settings.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { connect } from "react-redux"; +import { useHistory } from "react-router-dom"; +import { AppState } from "reducers"; +import { getCurrentOrg } from "selectors/organizationSelectors"; +import { ORG_INVITE_USERS_PAGE_URL } from "constants/routes"; +import EditableText from "pages/common/EditableText"; +import PageSectionDivider from "pages/common/PageSectionDivider"; +import PageSectionHeader from "pages/common/PageSectionHeader"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import Button from "components/editorComponents/Button"; +import { Org } from "constants/orgConstants"; + +export type PageProps = { + org?: Org; + changeOrgName: (value: string) => void; +}; + +export const OrgSettings = (props: PageProps) => { + const changeOrgName = (value: string) => { + if (props.org && value.trim().length > 0 && value !== props.org.name) { + props.changeOrgName(value); + } + }; + + const history = useHistory(); + + return ( + + +

+ {props.org && ( + + )} +

+
+ + +

Organization Users

+