Merge branch 'feature/invite-users' into 'release'

Invite Users

Organization Dropdown
=====================
- User must be able to see 
  - The organization dropdown in the headers
  - The name of the organization in the header
  - A list of navigation options on clicking the header; in a dropdown
  - Navigate to the organization settings page on clicking "Organization Settings"
  - View a list of organizations on clicking "Switch Organization"

Invite Users Form
=================
- User must be able to
  - View the Invite Users header
  - Go back to the previous page on clicking "Cancel"
  - Add a list of email addresses in the User emails field by "Enter", "Space" and ","
  - Delete individual email address - by clicking on the close icon, and "Backspace"
  - Select a role for the set of emails
  - See a validation message for each of the user email fields and role select fields
  - Delete an entry of the "role -> emails" set by clicking on the "delete" icon at the end of each sets of fields
  - Add another "role -> emails" set by clicking on "Add more"
  - Submit the list of sets of " role -> emails" by clicking on Invite Users button


Other changes
=============
- Each of the protected pages will check for login on load
- When logged in and trying to access "/" (base URL), user will be redirected to the applications page
- User can navigate to the invite users page from the "Organization settings" page.
- Add black favicon for scaffolding pages, and orange for editor


See merge request theappsmith/internal-tools-client!203
This commit is contained in:
Abhinav Jha 2019-12-23 12:16:33 +00:00
commit 78b56dd38b
82 changed files with 3006 additions and 961 deletions

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@ -10,11 +10,7 @@
<meta name="theme-color" content="#000000" />
<link href="https://fonts.googleapis.com/css?family=DM+Sans:400,500,700&display=swap" rel="stylesheet" />
<link href="../node_modules/normalize.css/normalize.css" rel="stylesheet" />
<!-- blueprint-icons.css file must be included alongside blueprint.css! -->
<link href="../node_modules/@blueprintjs/icons/lib/css/blueprint-icons.css" rel="stylesheet" />
<link href="../node_modules/@blueprintjs/core/lib/css/blueprint.css" rel="stylesheet" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Appsmith</title>

View File

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

View File

@ -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(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -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 (
<div className="App">
<Helmet>
<title>Appsmith</title>
<link rel="canonical" href="https://app.appsmith.com" />
</Helmet>
<header className="App-header">
<p>Coming Soon</p>
</header>
</div>
);
}
}
export const App = () => {
const currentUser = useSelector(state => state.ui.users.current);
return currentUser ? <Redirect to={APPLICATIONS_URL} /> : null;
};
export default App;

View File

@ -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<FetchOrgRolesResponse> {
return Api.get(OrgApi.rolesURL);
}
static fetchOrgs(): AxiosPromise<FetchOrgsResponse> {
return Api.get(OrgApi.orgsURL);
}
static fetchOrg(request: FetchOrgRequest): AxiosPromise<FetchOrgResponse> {
return Api.get(OrgApi.orgsURL + "/" + request.orgId);
}
static saveOrg(request: SaveOrgRequest): AxiosPromise<ApiResponse> {
return Api.put(OrgApi.orgsURL + "/" + request.id, request);
}
}
export default OrgApi;

View File

@ -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<CreateUserResponse> {
@ -62,6 +79,15 @@ class UserApi extends Api {
): AxiosPromise<ApiResponse> {
return Api.get(UserApi.verifyResetPasswordTokenURL, request);
}
static fetchUser(request: FetchUserRequest): AxiosPromise<FetchUserResponse> {
return Api.get(UserApi.fetchUserURL + "/" + request.id);
}
static inviteUser(request: InviteUserRequest): AxiosPromise<ApiResponse> {
request.status = "INVITED";
return Api.post(UserApi.inviteUserURL, request);
}
}
export default UserApi;

View File

@ -0,0 +1,3 @@
<svg width="15" height="20" viewBox="0 0 15 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.32031 20H14.1797C14.4141 20 14.6094 19.9219 14.7656 19.7656C14.9219 19.6094 15 19.4141 15 19.1797V1.67969C15 1.21094 14.8372 0.813802 14.5117 0.488281C14.1862 0.16276 13.7891 0 13.3203 0H1.67969C1.21094 0 0.813802 0.16276 0.488281 0.488281C0.16276 0.813802 0 1.21094 0 1.67969V19.1797C0 19.4141 0.078125 19.6094 0.234375 19.7656C0.390625 19.9219 0.585938 20 0.820312 20H3.32031ZM6.67969 18.3203H5V15H6.67969V18.3203ZM10 18.3203H8.32031V15H10V18.3203ZM1.67969 1.67969H13.3203V18.3203H11.6797V15C11.6797 14.5573 11.5104 14.1667 11.1719 13.8281C10.8333 13.4896 10.4427 13.3203 10 13.3203H5C4.55729 13.3203 4.16667 13.4896 3.82812 13.8281C3.48958 14.1667 3.32031 14.5573 3.32031 15V18.3203H1.67969V1.67969ZM3.32031 3.32031H5V5H3.32031V3.32031ZM6.67969 3.32031H8.32031V5H6.67969V3.32031ZM10 3.32031H11.6797V5H10V3.32031ZM3.32031 6.67969H5V8.32031H3.32031V6.67969ZM6.67969 6.67969H8.32031V8.32031H6.67969V6.67969ZM10 6.67969H11.6797V8.32031H10V6.67969ZM3.32031 10H5V11.6797H3.32031V10ZM6.67969 10H8.32031V11.6797H6.67969V10ZM10 10H11.6797V11.6797H10V10Z" fill="#768896"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,5 +1,6 @@
import React from "react";
import Select from "react-select";
import { WrappedFieldInputProps } from "redux-form";
import { theme } from "constants/DefaultTheme";

View File

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

View File

@ -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 (
<StyledAnchorButton
icon={icon}
rightIcon={rightIcon}
{...baseProps}
href={props.href}
/>
);
} else
return (
<StyledButton
rightIcon={rightIcon}
icon={icon}
{...baseProps}
onClick={props.onClick}
/>
);
};
export default Button;

View File

@ -1,8 +0,0 @@
import styled from "styled-components";
import { FormGroup } from "@blueprintjs/core";
const StyledFormGroup = styled(FormGroup)`
& {
width: 100%;
}
`;
export default StyledFormGroup;

View File

@ -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 <CustomizedDropdown {...dropdownProps} />;
};
export default SelectComponent;

View File

@ -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<string[]>(_values || []);
const [currentValue, setCurrentValue] = useState<string>("");
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 (
<TagInputWrapper intent={props.intent}>
<TagInput
inputProps={{ type: props.type, value: currentValue }}
onInputChange={handleInputChange}
placeholder={props.placeholder}
values={_values || [""]}
separator={props.separator || ","}
addOnBlur
addOnPaste
onChange={onTagsChange}
onKeyDown={onKeyDown}
tagProps={{
round: true,
}}
large
/>
</TagInputWrapper>
);
};
export default TagInputComponent;

View File

@ -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 <StyledError show={!!props.error}>{props.error}</StyledError>;
};
export default FormFieldError;

View File

@ -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)<FormActionButtonProps>`
${BlueprintButtonIntentsCSS}
`;

View File

@ -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 (
<FormFooterContainer>
{props.divider && <Divider />}
<FooterActions>
{props.onCancel && (
<FormActionButton
text={props.cancelText || "Cancel"}
type="button"
onClick={props.onCancel}
large
/>
)}
{props.onSubmit && (
<FormActionButton
text={props.submitText || "Submit"}
type={props.submitOnEnter ? "submit" : "button"}
intent="primary"
onClick={props.onSubmit}
loading={props.submitting}
large
/>
)}
</FooterActions>
</FormFooterContainer>
);
};
export default FormFooter;

View File

@ -0,0 +1,11 @@
import styled from "styled-components";
import { FormGroup } from "@blueprintjs/core";
type FormGroupProps = {
fill?: boolean;
};
const StyledFormGroup = styled(FormGroup)<FormGroupProps>`
& {
width: ${props => (props.fill ? "100%" : "auto")};
}
`;
export default StyledFormGroup;

View File

@ -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 => <ActionButton key={action.text} {...action} />);
@ -83,4 +83,4 @@ export const MessageTag = (props: MessageTagProps) => {
);
};
export default MessageTag;
export default FormMessage;

View File

@ -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<WrappedFieldMetaProps>;
input: Partial<WrappedFieldInputProps>;
},
) => {
return (
<React.Fragment>
<SelectComponent {...componentProps} />
<FormFieldError
error={componentProps.meta.touched && componentProps.meta.error}
/>
</React.Fragment>
);
};
type SelectFieldProps = {
name: string;
placeholder?: string;
options?: Array<{ id: string; name: string; value?: string }>;
};
export const SelectField = (props: SelectFieldProps) => {
return (
<Field
name={props.name}
placeholder={props.placeholder}
component={renderComponent}
options={props.options}
/>
);
};
export default SelectField;

View File

@ -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<WrappedFieldMetaProps>;
input: Partial<WrappedFieldInputProps>;
},
) => {
return (
<React.Fragment>
<TagInputComponent {...componentProps} />
<FormFieldError
error={componentProps.meta.touched && componentProps.meta.error}
/>
</React.Fragment>
);
};
type TagListFieldProps = {
name: string;
placeholder: string;
type: string;
label: string;
intent: Intent;
};
const TagListField = (props: TagListFieldProps) => {
return (
<React.Fragment>
<Field component={renderComponent} {...props} />
</React.Fragment>
);
};
export default TagListField;

View File

@ -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<string, Color> = {
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<string, Color> = {
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 };

View File

@ -12,6 +12,7 @@ export const IconWrapper = styled.div<IconProps>`
&: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;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
export type OrgRole = {
id: string;
name: string;
};
export type Org = {
id: string;
name: string;
website?: string;
};

View File

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

View File

@ -0,0 +1,6 @@
export type User = {
id: string;
email: string;
currentOrganizationId: string;
organizationIds: string[];
};

View File

@ -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: {
<ApisIcon />
</IconWrapper>
),
ORG_ICON: (props: IconProps) => (
<IconWrapper {...props}>
<OrgIcon />
</IconWrapper>
),
};

View File

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

View File

@ -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(
<DndProvider backend={HTML5Backend}>
<Provider store={store}>
@ -50,15 +39,16 @@ ReactDOM.render(
<Router history={history}>
<Suspense fallback={loadingIndicator}>
<Switch>
<Route exact path={BASE_URL} component={App} />
<ProtectedRoute exact path={BASE_URL} component={App} />
<ProtectedRoute path={ORG_URL} component={Organization} />
<Route path={USER_AUTH_URL} component={UserAuth} />
<ProtectedRoute path={BUILDER_URL} component={Editor} />
<ProtectedRoute path={APP_VIEW_URL} component={AppViewer} />
<ProtectedRoute
exact
path={APPLICATIONS_URL}
component={Applications}
/>
<ProtectedRoute path={BUILDER_URL} component={Editor} />
<ProtectedRoute path={APP_VIEW_URL} component={AppViewer} />
<Route component={PageNotFound} />
</Switch>
</Suspense>

View File

@ -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)<IIconProps>`
&&& {
background: ${props => props.theme.colors.primary};
span {
color: white;
}
}
`;
type ApplicationsHeaderProps = {
add?: {
form: JSX.Element;
title: string;
};
};
export const ApplicationsHeader = (props: ApplicationsHeaderProps) => {
return (
<StyledHeader>
{props.add && (
<Popover
interactionKind={PopoverInteractionKind.CLICK}
popoverClassName="bp3-popover-content-sizing"
position={Position.BOTTOM}
>
<StyledAddButton
text={props.add.title}
icon="plus"
title={props.add.title}
minimal
large
/>
{props.add.form}
</Popover>
)}
</StyledHeader>
);
};
export default ApplicationsHeader;

View File

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

View File

@ -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<ApplicationProps> {
? getApplicationPayloads(4)
: this.props.applicationList;
return (
<ApplicationsPageWrapper>
<Helmet>
<meta charSet="utf-8" />
<title>Applications | Appsmith</title>
</Helmet>
<ApplicationsHeader />
<ApplicationsBody>
<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) => {
return (
application.pageCount > 0 && (
<ApplicationCard
key={application.id}
loading={this.props.isFetchingApplications}
application={application}
share={noop}
duplicate={noop}
delete={noop}
/>
)
);
})}
</ApplicationCardsWrapper>
</ApplicationsBody>
</ApplicationsPageWrapper>
<PageWrapper displayName="Applications">
<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",
}}
/>
<PageSectionDivider />
<ApplicationCardsWrapper>
{applicationList.map((application: ApplicationPayload) => {
return (
application.pageCount > 0 && (
<ApplicationCard
key={application.id}
loading={this.props.isFetchingApplications}
application={application}
share={noop}
duplicate={noop}
delete={noop}
/>
)
);
})}
</ApplicationCardsWrapper>
</PageWrapper>
);
}
}

View File

@ -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");
}
}
`;

View File

@ -103,6 +103,7 @@ class Editor extends Component<EditorProps> {
<Helmet>
<meta charSet="utf-8" />
<title>Editor | Appsmith</title>
<link rel="shortcut icon" href="/favicon-orange.ico" />
</Helmet>
<EditorHeader
isSaving={this.props.isSaving}

View File

@ -20,10 +20,10 @@ import {
FORGOT_PASSWORD_SUCCESS_TEXT,
} from "constants/messages";
import MessageTag from "components/editorComponents/form/MessageTag";
import FormMessage from "components/editorComponents/form/FormMessage";
import { FORGOT_PASSWORD_FORM_NAME } from "constants/forms";
import FormGroup from "components/editorComponents/FormGroup";
import FormGroup from "components/editorComponents/form/FormGroup";
import FormButton from "components/editorComponents/FormButton";
import TextField from "components/editorComponents/form/fields/TextField";
import { isEmail, isEmptyString } from "utils/formhelpers";
@ -62,12 +62,12 @@ export const ForgotPassword = (props: ForgotPasswordProps) => {
return (
<AuthCardContainer>
{submitSucceeded && (
<MessageTag
<FormMessage
intent="success"
message={`${FORGOT_PASSWORD_SUCCESS_TEXT} ${props.emailValue}`}
/>
)}
{submitFailed && error && <MessageTag intent="danger" message={error} />}
{submitFailed && error && <FormMessage intent="danger" message={error} />}
<AuthCardHeader>
<h1>{FORGOT_PASSWORD_PAGE_TITLE}</h1>
<h5>{FORGOT_PASSWORD_PAGE_SUBTITLE}</h5>

View File

@ -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 (
<AuthCardContainer>
{showError && pristine && (
<MessageTag
<FormMessage
intent="danger"
message={LOGIN_PAGE_INVALID_CREDS_ERROR}
actions={[

View File

@ -8,13 +8,13 @@ import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { getIsTokenValid, getIsValidatingToken } from "selectors/authSelectors";
import { Icon } from "@blueprintjs/core";
import TextField from "components/editorComponents/form/fields/TextField";
import MessageTag, {
MessageTagProps,
import FormMessage, {
FormMessageProps,
MessageAction,
} from "components/editorComponents/form/MessageTag";
} from "components/editorComponents/form/FormMessage";
import Spinner from "components/editorComponents/Spinner";
import FormButton from "components/editorComponents/FormButton";
import FormGroup from "components/editorComponents/FormGroup";
import FormGroup from "components/editorComponents/form/FormGroup";
import StyledForm from "components/editorComponents/Form";
import { isEmptyString, isStrongPassword } from "utils/formhelpers";
@ -126,7 +126,7 @@ export const ResetPassword = (props: ResetPasswordProps) => {
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 <MessageTag {...messageTagProps} />;
return <FormMessage {...messageTagProps} />;
}
if (!isTokenValid && validatingToken) {
@ -145,7 +145,7 @@ export const ResetPassword = (props: ResetPasswordProps) => {
return (
<AuthCardContainer>
{(showSuccessMessage || showFailureMessage) && (
<MessageTag {...messageTagProps} />
<FormMessage {...messageTagProps} />
)}
<AuthCardHeader>
<h1>{RESET_PASSWORD_PAGE_TITLE}</h1>

View File

@ -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<SignupFormValues>) => {
return (
<AuthCardContainer>
{submitSucceeded && (
<MessageTag
<FormMessage
intent="success"
message={SIGNUP_PAGE_SUCCESS}
actions={[
@ -81,7 +81,7 @@ export const SignUp = (props: InjectedFormProps<SignupFormValues>) => {
]}
/>
)}
{submitFailed && error && <MessageTag intent="danger" message={error} />}
{submitFailed && error && <FormMessage intent="danger" message={error} />}
<AuthCardHeader>
<h1>{SIGNUP_PAGE_TITLE}</h1>
<h5>{SIGNUP_PAGE_SUBTITLE}</h5>

View File

@ -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 (
<BadgeWrapper>
<img src={props.imageURL} alt={props.text}></img>
<div>
<h3>{props.text}</h3>
<h5>{props.subtext}</h5>
</div>
</BadgeWrapper>
);
};
export default Badge;

View File

@ -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: (
<Badge
text={currentOrg.name}
subtext="2 Projects"
imageURL="https://via.placeholder.com/32"
/>
),
active: false,
},
{
content: <Link to="/org/settings">Organization Settings</Link>,
},
{
content: <Link to="/org/users">Members</Link>,
},
{
content: <Link to="/org/biling">Usage & Billing</Link>,
},
{
content: <Link to="/org/support">Support</Link>,
},
{
content: <DropdownComponent {...switchdropdown(orgs)} />,
shouldCloseDropdown: false,
},
{
content: "Switch To Personal Workspace",
onSelect: () =>
getOnSelectAction(DropdownOnSelectActions.DISPATCH, {
type: ReduxActionTypes.SWITCH_ORGANIZATION_INIT,
payload: { organizationId: currentOrg.id },
}),
},
],
},
{
options: [
{
content: (
<Badge
text={user.email}
subtext={user.email}
imageURL="https://via.placeholder.com/32"
/>
),
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;

View File

@ -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 : ``)};
`;

View File

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

View File

@ -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 (
<React.Fragment>
{section.options &&
section.options.map((option, index) => {
const shouldClose =
option.shouldCloseDropdown === undefined ||
option.shouldCloseDropdown;
return (
<Option
key={index}
className={shouldClose ? Classes.POPOVER_DISMISS : ""}
onClick={option.onSelect}
active={option.active === undefined ? true : option.active}
>
{option.content}
</Option>
);
})}
</React.Fragment>
);
};
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 = (
<React.Fragment>
{icon && <div>{icon}</div>}
{props.trigger.content || (
<Button
outline={props.trigger.outline}
filled={props.trigger.filled}
icon={getDirectionBased.ICON_NAME(props.openDirection) as IconName}
iconAlignment={Directions.RIGHT}
text={props.trigger.text}
intent={props.trigger.intent}
/>
)}
</React.Fragment>
);
const content = props.sections.map((section, index) => (
<DropdownContentSection key={index} stick={!!section.isSticky}>
{getContentSection(section)}
</DropdownContentSection>
));
return (
<Popover
position={
getDirectionBased.POPPER_POSITION(
props.openDirection,
) as PopoverPosition
}
interactionKind={
props.openOnHover
? PopoverInteractionKind.HOVER
: PopoverInteractionKind.CLICK
}
minimal
>
<DropdownTrigger>{trigger}</DropdownTrigger>
<DropdownContent>{content}</DropdownContent>
</Popover>
);
};
export default withTheme(CustomizedDropdown);

View File

@ -0,0 +1,26 @@
import React, { useState } from "react";
import { EditableText as BlueprintEditableText } from "@blueprintjs/core";
type EditableTextProps = {
type: "text" | "password" | "email" | "phone" | "date";
defaultValue: string;
onTextChanged: (value: string) => void;
};
export const EditableText = (props: EditableTextProps) => {
const [isEditing, setIsEditing] = useState(false);
const edit = () => setIsEditing(true);
return (
<div onDoubleClick={edit}>
<BlueprintEditableText
disabled={!isEditing}
isEditing={isEditing}
{...props}
onConfirm={props.onTextChanged}
selectAllOnFocus
/>
</div>
);
};
export default EditableText;

View File

@ -0,0 +1,40 @@
import React from "react";
import { connect } from "react-redux";
import { getCurrentUser } from "selectors/usersSelectors";
import { getOrgs, getCurrentOrg } from "selectors/organizationSelectors";
import styled from "styled-components";
import StyledHeader from "components/designSystems/appsmith/StyledHeader";
import CustomizedDropdown from "./CustomizedDropdown";
import DropdownProps from "./CustomizedDropdown/OrgDropdownData";
import { AppState } from "reducers";
import { Org } from "constants/orgConstants";
import { User } from "constants/userConstants";
const StyledPageHeader = styled(StyledHeader)`
justify-content: space-between;
`;
type PageHeaderProps = {
orgs?: Org[];
currentOrg?: Org;
user?: User;
};
export const PageHeader = (props: PageHeaderProps) => {
const { orgs, currentOrg, user } = props;
return (
<StyledPageHeader>
{orgs && user && currentOrg && (
<CustomizedDropdown {...DropdownProps(orgs, currentOrg, user)} />
)}
</StyledPageHeader>
);
};
const mapStateToProps = (state: AppState) => ({
currentOrg: getCurrentOrg(state),
user: getCurrentUser(state),
orgs: getOrgs(state),
});
export default connect(mapStateToProps, null)(PageHeader);

View File

@ -0,0 +1,7 @@
import styled from "styled-components";
import { Divider } from "@blueprintjs/core";
export default styled(Divider)`
margin: ${props => props.theme.spaces[11]}px auto;
width: 100%;
`;

View File

@ -0,0 +1,8 @@
import styled from "styled-components";
export default styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
align-items: center;
`;

View File

@ -0,0 +1,61 @@
import React, { ReactNode } from "react";
import { Helmet } from "react-helmet";
import styled from "styled-components";
import PageHeader from "./PageHeader";
const Wrapper = styled.section`
&& .fade {
position: relative;
}
&& .fade-enter {
opacity: 0;
z-index: 1;
}
&& .fade-enter.fade-enter-active {
opacity: 1;
transition: opacity 150ms ease-in;
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
display: none;
opacity: 0;
}
`;
const PageBody = styled.div`
width: ${props => props.theme.pageContentWidth}px;
min-height: calc(
100vh - ${props => props.theme.headerHeight + props.theme.spaces[12]}
);
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};
& > * {
width: 100%;
}
`;
type PageWrapperProps = {
children?: ReactNode;
displayName?: string;
};
export const PageWrapper = (props: PageWrapperProps) => (
<Wrapper>
<Helmet>
<meta charSet="utf-8" />
<title>{`${props.displayName} | Appsmith`}</title>
<link rel="shortcut icon" href="/favicon-black.ico" />
</Helmet>
<PageHeader />
<PageBody>{props.children}</PageBody>
</Wrapper>
);
export default PageWrapper;

View File

@ -1,5 +1,30 @@
import * as React from "react";
import React from "react";
import { Route } from "react-router-dom";
import { useDispatch } from "react-redux";
import { useSelector } from "store";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { hasAuthExpired } from "utils/storage";
import { User } from "constants/userConstants";
export const checkAuth = (dispatch: any, currentUser?: User) => {
return hasAuthExpired().then(hasExpired => {
if (!currentUser || hasExpired) {
dispatch({
type: ReduxActionTypes.SET_CURRENT_USER_INIT,
payload: {
id: "me",
},
});
}
});
};
export const WrappedComponent = (props: any) => {
const dispatch = useDispatch();
const currentUser = useSelector(state => state.ui.users.current);
checkAuth(dispatch, currentUser);
return currentUser ? props.children : null;
};
const ProtectedRoute = ({
component: Component,
@ -9,7 +34,16 @@ const ProtectedRoute = ({
component: React.ReactType;
exact?: boolean;
}) => {
return <Route {...rest} render={props => <Component {...props} />} />;
return (
<Route
{...rest}
render={props => (
<WrappedComponent {...props}>
<Component {...props} />
</WrappedComponent>
)}
/>
);
};
export default ProtectedRoute;

View File

@ -0,0 +1,246 @@
import React, { useLayoutEffect } from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { useHistory } from "react-router-dom";
import {
FieldArray,
reduxForm,
InjectedFormProps,
WrappedFieldArrayProps,
} from "redux-form";
import FormMessage from "components/editorComponents/form/FormMessage";
import { INVITE_USERS_TO_ORG_FORM } from "constants/forms";
import {
INVITE_USERS_VALIDATION_EMAIL_LIST,
INVITE_USERS_VALIDATION_ROLE_EMPTY,
INVITE_USERS_EMAIL_LIST_LABEL,
INVITE_USERS_EMAIL_LIST_PLACEHOLDER,
INVITE_USERS_ROLE_SELECT_LABEL,
INVITE_USERS_ROLE_SELECT_PLACEHOLDER,
INVITE_USERS_ADD_EMAIL_LIST_FIELD,
INVITE_USERS_SUBMIT_BUTTON_TEXT,
INVITE_USERS_SUBMIT_ERROR,
INVITE_USERS_SUBMIT_SUCCESS,
INVITE_USERS_VALIDATION_EMAILS_EMPTY,
} from "constants/messages";
import {
InviteUsersToOrgFormValues,
InviteUsersToOrgByRoleValues,
inviteUsersToOrgSubmitHandler,
} from "./helpers";
import { generateReactKey } from "utils/generators";
import TagListField from "components/editorComponents/form/fields/TagListField";
import { FormIcons } from "icons/FormIcons";
import FormFooter from "components/editorComponents/form/FormFooter";
import FormActionButton from "components/editorComponents/form/FormActionButton";
import FormGroup from "components/editorComponents/form/FormGroup";
import SelectField from "components/editorComponents/form/fields/SelectField";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { AppState } from "reducers";
import { getRoles } from "selectors/organizationSelectors";
import { OrgRole } from "constants/orgConstants";
import { isEmail } from "utils/formhelpers";
const validate = (values: InviteUsersToOrgFormValues) => {
const errors: any = { usersByRole: [] };
if (values.usersByRole && values.usersByRole.length) {
values.usersByRole.forEach((role, index) => {
errors.usersByRole[index] = { id: "", users: "", role: "" };
// If we have users entered for a role.
if (role.users && role.users.length > 0) {
// Split the users CSV string to an array.
const _users = role.users.split(",").filter(Boolean);
// Check if each entry is an email
_users.forEach(user => {
if (!isEmail(user)) {
if (errors.usersByRole[index].users)
errors.usersByRole[index].users += `${user}, `;
else errors.usersByRole[index].users = `${user}, `;
}
});
if (
errors.usersByRole[index].users &&
errors.usersByRole[index].users.length > 0
) {
errors.usersByRole[
index
].users = `${INVITE_USERS_VALIDATION_EMAIL_LIST} ${errors.usersByRole[
index
].users.slice(0, -2)}`;
}
// Check if role has been specified
if (role.role === undefined || role.role?.trim().length === 0) {
errors.usersByRole[index].role = INVITE_USERS_VALIDATION_ROLE_EMPTY;
}
} else {
errors.usersByRole[index].users = INVITE_USERS_VALIDATION_EMAILS_EMPTY;
}
});
}
return errors;
};
const StyledForm = styled.form`
width: 100%;
background: white;
padding: ${props => props.theme.spaces[11]}px;
`;
const StyledInviteFieldGroup = styled.div`
&& {
display: flex;
flex-direction: row;
flex-wrap: none;
justify-content: space-between;
align-items: flex-start;
& > div:first-of-type {
}
& > div {
min-width: 150px;
margin: 0em 1em 1em 0em;
}
& > div:last-of-type {
min-width: 0;
display: flex;
align-self: center;
}
}
`;
const renderInviteUsersByRoleForm = (
renderer: WrappedFieldArrayProps<InviteUsersToOrgByRoleValues> & {
roles?: OrgRole[];
},
) => {
const { fields, roles } = renderer;
return (
<React.Fragment>
{fields.map((field, index) => {
return (
<StyledInviteFieldGroup key={`${field}.id`}>
<FormGroup fill label={INVITE_USERS_EMAIL_LIST_LABEL}>
<TagListField
name={`${field}.users`}
placeholder={INVITE_USERS_EMAIL_LIST_PLACEHOLDER}
type="email"
label="Emails"
intent="success"
/>
</FormGroup>
{roles && (
<FormGroup label={INVITE_USERS_ROLE_SELECT_LABEL}>
<SelectField
name={`${field}.role`}
placeholder={INVITE_USERS_ROLE_SELECT_PLACEHOLDER}
options={roles}
/>
</FormGroup>
)}
<FormIcons.DELETE_ICON
width={32}
height={32}
style={{
cursor: "pointer",
}}
onClick={() => fields.remove(index)}
/>
</StyledInviteFieldGroup>
);
})}
<FormActionButton
onClick={() => fields.push({ id: generateReactKey() })}
text={INVITE_USERS_ADD_EMAIL_LIST_FIELD}
large
icon="plus"
/>
</React.Fragment>
);
};
type InviteUsersFormProps = InjectedFormProps<
InviteUsersToOrgFormValues,
{
fetchRoles: () => void;
roles?: OrgRole[];
}
> & {
fetchRoles: () => void;
roles?: OrgRole[];
};
export const InviteUsersForm = (props: InviteUsersFormProps) => {
const {
handleSubmit,
submitting,
submitFailed,
submitSucceeded,
error,
fetchRoles,
roles,
} = props;
const history = useHistory();
useLayoutEffect(() => {
if (!roles) {
fetchRoles();
}
}, [fetchRoles, roles]);
return (
<StyledForm>
{submitSucceeded && (
<FormMessage intent="success" message={INVITE_USERS_SUBMIT_SUCCESS} />
)}
{submitFailed && error && (
<FormMessage
intent="danger"
message={`${INVITE_USERS_SUBMIT_ERROR}: ${error}`}
/>
)}
{/* Disable submit on "Enter" because TagInputComponent uses it. */}
<button
type="submit"
disabled
style={{ display: "none" }}
aria-hidden="true"
></button>
<FieldArray
name="usersByRole"
component={renderInviteUsersByRoleForm}
props={{ roles: roles }}
/>
<FormFooter
divider
onSubmit={handleSubmit(inviteUsersToOrgSubmitHandler)}
submitting={submitting && !submitFailed}
onCancel={() => history.goBack()}
submitOnEnter={false}
submitText={INVITE_USERS_SUBMIT_BUTTON_TEXT}
></FormFooter>
</StyledForm>
);
};
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),
);

View File

@ -0,0 +1 @@
export default "Create Organization";

View File

@ -0,0 +1,8 @@
import React from "react";
import Centered from "components/designSystems/appsmith/CenteredWrapper";
export const DefaultOrgPage = () => (
<Centered>
<p>This page is under construction</p>
</Centered>
);
export default DefaultOrgPage;

View File

@ -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<any> => {
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);
});
};

View File

@ -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 (
<PageWrapper displayName="Organization Settings">
<TransitionGroup>
<CSSTransition key={location.key} classNames="fade" timeout={300}>
<Switch location={location}>
<Route exact path={`${path}/settings`} component={Settings} />
<Route exact path={`${path}/users`} component={Users} />
<Route exact path={`${path}/invite`} component={Invite} />
<Route component={DefaultOrgPage} />
</Switch>
</CSSTransition>
</TransitionGroup>
</PageWrapper>
);
};
export default Organization;

View File

@ -0,0 +1,14 @@
import React from "react";
import InviteUsersForm from "./InviteUsersForm";
export const Invite = () => {
// const handleInviteUsersSubmit = (values: InviteUsersFormValues) => {};
return (
<React.Fragment>
<h1>Invite Users</h1>
<InviteUsersForm />
</React.Fragment>
);
};
export default Invite;

View File

@ -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 (
<React.Fragment>
<PageSectionHeader>
<h1>
{props.org && (
<EditableText
defaultValue={props.org.name}
type="text"
onTextChanged={changeOrgName}
/>
)}
</h1>
</PageSectionHeader>
<PageSectionDivider />
<PageSectionHeader>
<h1>Organization Users</h1>
<Button
intent="primary"
text="Invite Users"
icon="plus"
iconAlignment="left"
filled
onClick={() => history.push(ORG_INVITE_USERS_PAGE_URL)}
/>
</PageSectionHeader>
</React.Fragment>
);
};
const mapStateToProps = (state: AppState) => ({
org: getCurrentOrg(state),
});
const mapDispatchToProps = (dispatch: any) => ({
changeOrgName: (name: string) =>
dispatch({
type: ReduxActionTypes.UPDATE_ORG_NAME_INIT,
payload: {
name,
},
}),
deleteOrg: (orgId: string) =>
dispatch({
type: ReduxActionTypes.DELETE_ORG_INIT,
payload: {
orgId,
},
}),
});
export default connect(mapStateToProps, mapDispatchToProps)(OrgSettings);

View File

@ -0,0 +1,3 @@
export const Users = () => null;
export default Users;

View File

@ -0,0 +1 @@
export default "list users";

View File

@ -0,0 +1 @@
export default "view user";

View File

@ -21,6 +21,8 @@ import { ApiPaneReduxState } from "./uiReducers/apiPaneReducer";
import { RoutesParamsReducerState } from "reducers/uiReducers/routesParamsReducer";
import { PluginDataState } from "reducers/entityReducers/pluginsReducer";
import { AuthState } from "reducers/uiReducers/authReducer";
import { OrgReduxState } from "reducers/uiReducers/orgReducer";
import { UsersReduxState } from "reducers/uiReducers/usersReducer";
const appReducer = combineReducers({
entities: entityReducer,
@ -41,6 +43,8 @@ export interface AppState {
apiPane: ApiPaneReduxState;
routesParams: RoutesParamsReducerState;
auth: AuthState;
orgs: OrgReduxState;
users: UsersReduxState;
};
entities: {
canvasWidgets: CanvasWidgetsReduxState;

View File

@ -8,6 +8,8 @@ import { widgetSidebarReducer } from "./widgetSidebarReducer";
import apiPaneReducer from "./apiPaneReducer";
import routesParamsReducer from "reducers/uiReducers/routesParamsReducer";
import authReducer from "./authReducer";
import orgReducer from "./orgReducer";
import usersReducer from "./usersReducer";
const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer,
@ -19,5 +21,7 @@ const uiReducer = combineReducers({
apiPane: apiPaneReducer,
routesParams: routesParamsReducer,
auth: authReducer,
orgs: orgReducer,
users: usersReducer,
});
export default uiReducer;

View File

@ -0,0 +1,60 @@
import { createReducer } from "utils/AppsmithUtils";
import {
ReduxAction,
ReduxActionTypes,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { OrgRole, Org } from "constants/orgConstants";
const initialState: OrgReduxState = {
loadingStates: {
fetchingRoles: false,
},
};
const orgReducer = createReducer(initialState, {
[ReduxActionTypes.FETCH_ORG_ROLES_INIT]: (state: OrgReduxState) => ({
...state,
roles: undefined,
loadingStates: {
...state.loadingStates,
fetchingRoles: true,
},
}),
[ReduxActionTypes.FETCH_ORG_ROLES_SUCCESS]: (
state: OrgReduxState,
action: ReduxAction<OrgRole[]>,
) => ({
...state,
roles: action.payload,
loadingStates: {
...state.loadingStates,
fetchingRoles: false,
},
}),
[ReduxActionErrorTypes.FETCH_ORG_ROLES_ERROR]: (state: OrgReduxState) => ({
...state,
roles: undefined,
loadingStates: {
...state.loadingStates,
fetchingRoles: false,
},
}),
[ReduxActionTypes.FETCH_ORGS_SUCCESS]: (
state: OrgReduxState,
action: ReduxAction<Org[]>,
) => ({
...state,
list: action.payload,
}),
});
export interface OrgReduxState {
list?: Org[];
roles?: OrgRole[];
loadingStates: {
fetchingRoles: boolean;
};
}
export default orgReducer;

View File

@ -0,0 +1,69 @@
import _ from "lodash";
import { createReducer } from "utils/AppsmithUtils";
import {
ReduxAction,
ReduxActionTypes,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { User } from "constants/userConstants";
const initialState: UsersReduxState = {
loadingStates: {
fetchingUsers: false,
fetchingUser: false,
},
list: [],
};
const usersReducer = createReducer(initialState, {
[ReduxActionTypes.FETCH_USER_INIT]: (state: UsersReduxState) => ({
...state,
loadingStates: {
...state.loadingStates,
fetchingUser: true,
},
}),
[ReduxActionTypes.FETCH_USER_SUCCESS]: (
state: UsersReduxState,
action: ReduxAction<User>,
) => {
const users = [...state.list];
const userIndex = _.findIndex(users, { id: action.payload.id });
if (userIndex > -1) {
users[userIndex] = action.payload;
} else {
users.push(action.payload);
}
return {
...state,
loadingStates: {
...state.loadingStates,
fetchingUser: false,
},
list: users,
};
},
[ReduxActionErrorTypes.FETCH_USER_ERROR]: (state: UsersReduxState) => ({
...state,
loadingStates: { ...state.loadingStates, fetchingUser: false },
}),
[ReduxActionTypes.SET_CURRENT_USER_SUCCESS]: (
state: UsersReduxState,
action: ReduxAction<User>,
) => ({
...state,
current: action.payload,
}),
});
export interface UsersReduxState {
current?: User;
list: User[];
loadingStates: {
fetchingUser: boolean;
fetchingUsers: boolean;
};
}
export default usersReducer;

View File

@ -0,0 +1,109 @@
import { call, takeLatest, put, all } from "redux-saga/effects";
import {
ReduxActionTypes,
ReduxAction,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { validateResponse } from "sagas/ErrorSagas";
import OrgApi, {
FetchOrgRolesResponse,
FetchOrgsResponse,
SaveOrgRequest,
FetchOrgRequest,
FetchOrgResponse,
} from "api/OrgApi";
import { ApiResponse } from "api/ApiResponses";
export function* fetchRolesSaga() {
try {
const response: FetchOrgRolesResponse = yield call(OrgApi.fetchRoles);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_ORG_ROLES_SUCCESS,
payload: response.data,
});
}
} catch (error) {
console.log(error);
yield put({
type: ReduxActionErrorTypes.FETCH_ORG_ROLES_ERROR,
payload: {
error,
},
});
}
}
export function* fetchOrgsSaga() {
try {
const response: FetchOrgsResponse = yield call(OrgApi.fetchOrgs);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_ORGS_SUCCESS,
payload: response.data,
});
}
} catch (error) {
console.log(error);
yield put({
type: ReduxActionErrorTypes.FETCH_ORGS_ERROR,
payload: {
error,
},
});
}
}
export function* fetchOrgSaga(action: ReduxAction<FetchOrgRequest>) {
try {
const request: FetchOrgRequest = action.payload;
const response: FetchOrgResponse = yield call(OrgApi.fetchOrg, request);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_ORG_SUCCESS,
payload: response.data,
});
}
} catch (error) {
console.log(error);
yield put({
type: ReduxActionErrorTypes.FETCH_ORG_ERROR,
payload: {
error,
},
});
}
}
export function* saveOrgSaga(action: ReduxAction<SaveOrgRequest>) {
try {
const request: SaveOrgRequest = action.payload;
const response: ApiResponse = yield call(OrgApi.saveOrg, request);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.SAVE_ORG_SUCCESS,
});
}
} catch (error) {
console.log(error);
yield put({
type: ReduxActionErrorTypes.SAVE_ORG_ERROR,
payload: {
error,
},
});
}
}
export default function* orgSagas() {
yield all([
takeLatest(ReduxActionTypes.FETCH_ORG_ROLES_INIT, fetchRolesSaga),
takeLatest(ReduxActionTypes.FETCH_ORG_INIT, fetchOrgSaga),
takeLatest(ReduxActionTypes.FETCH_ORGS_INIT, fetchOrgsSaga),
takeLatest(ReduxActionTypes.SAVE_ORG_INIT, saveOrgSaga),
]);
}

View File

@ -15,6 +15,7 @@ import watchActionWidgetMapSagas, {
import apiPaneSagas from "./ApiPaneSagas";
import userSagas from "./userSagas";
import pluginSagas from "./PluginSagas";
import orgSagas from "./OrgSagas";
export function* rootSaga() {
yield all([
@ -33,5 +34,6 @@ export function* rootSaga() {
spawn(apiPaneSagas),
spawn(userSagas),
spawn(pluginSagas),
spawn(orgSagas),
]);
}

View File

@ -10,6 +10,8 @@ import UserApi, {
ForgotPasswordRequest,
ResetPasswordRequest,
ResetPasswordVerifyTokenRequest,
FetchUserRequest,
FetchUserResponse,
} from "api/UserApi";
import { ApiResponse } from "api/ApiResponses";
import {
@ -18,6 +20,10 @@ import {
callAPI,
} from "./ErrorSagas";
import { fetchOrgsSaga } from "./OrgSagas";
import { resetAuthExpiration } from "utils/storage";
export function* createUserSaga(
action: ReduxAction<{
resolve: any;
@ -131,6 +137,67 @@ export function* resetPasswordSaga(
});
}
}
type InviteUserPayload = {
email: string;
groupIds: string[];
};
export function* inviteUser(payload: InviteUserPayload, reject: any) {
const response: ApiResponse = yield callAPI(UserApi.inviteUser, payload);
const isValidResponse = yield validateResponse(response);
if (!isValidResponse) {
let errorMessage = `${payload.email}: `;
errorMessage += getResponseErrorMessage(response);
yield call(reject, { _error: errorMessage });
}
yield;
}
export function* inviteUsers(
action: ReduxAction<{
data: Array<{ roleId: string; emails: string[] }>;
resolve: any;
reject: any;
}>,
) {
try {
const { data, resolve, reject } = action.payload;
const sagasToCall = [];
const emailSet: Record<string, string[]> = {};
data.forEach((groupSet: { roleId: string; emails: string[] }) => {
const { emails, roleId } = groupSet;
emails.forEach((email: string) => {
if (emailSet.hasOwnProperty(email)) {
emailSet[email].push(roleId);
} else {
emailSet[email] = [roleId];
}
});
});
for (const email in emailSet) {
sagasToCall.push(
call(inviteUser, { email, groupIds: emailSet[email] }, reject),
);
}
yield all(sagasToCall);
yield put({
type: ReduxActionTypes.INVITE_USERS_TO_ORG_SUCCESS,
payload: {
inviteCount: sagasToCall.length,
},
});
yield call(resolve);
} catch (error) {
console.log(error);
yield put({
type: ReduxActionErrorTypes.INVITE_USERS_TO_ORG_ERROR,
payload: {
error,
},
});
}
}
export function* verifyResetPasswordTokenSaga(
action: ReduxAction<{ token: string; email: string }>,
@ -155,6 +222,39 @@ export function* verifyResetPasswordTokenSaga(
}
}
export function* fetchUserSaga(action: ReduxAction<FetchUserRequest>) {
try {
const request: FetchUserRequest = action.payload;
const response: FetchUserResponse = yield call(UserApi.fetchUser, request);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_USER_SUCCESS,
payload: response.data,
});
return yield response.data;
}
return yield false;
} catch (error) {
console.log(error);
yield put({
type: ReduxActionErrorTypes.FETCH_USER_ERROR,
});
}
}
export function* setCurrentUserSaga(action: ReduxAction<FetchUserRequest>) {
const me = yield call(fetchUserSaga, action);
if (me) {
resetAuthExpiration();
yield put({
type: ReduxActionTypes.SET_CURRENT_USER_SUCCESS,
payload: me,
});
yield call(fetchOrgsSaga);
}
}
export default function* userSagas() {
yield all([
takeLatest(ReduxActionTypes.CREATE_USER_INIT, createUserSaga),
@ -164,5 +264,8 @@ export default function* userSagas() {
ReduxActionTypes.RESET_PASSWORD_VERIFY_TOKEN_INIT,
verifyResetPasswordTokenSaga,
),
takeLatest(ReduxActionTypes.INVITE_USERS_TO_ORG_INIT, inviteUsers),
takeLatest(ReduxActionTypes.FETCH_USER_INIT, fetchUserSaga),
takeLatest(ReduxActionTypes.SET_CURRENT_USER_INIT, setCurrentUserSaga),
]);
}

View File

@ -0,0 +1,22 @@
import { createSelector } from "reselect";
import { AppState } from "reducers";
import { OrgRole, Org } from "constants/orgConstants";
import _ from "lodash";
export const getOrgs = (state: AppState) => state.ui.orgs.list;
export const getCurrentUserOrgId = (state: AppState) =>
state.ui.users.current?.currentOrganizationId;
export const getCurrentOrg = createSelector(
getOrgs,
getCurrentUserOrgId,
(orgs?: Org[], id?: string) => {
if (orgs && id) {
return _.find(orgs, { id: id });
}
return undefined;
},
);
export const getRoles = (state: AppState): OrgRole[] | undefined => {
return state.ui.orgs.roles;
};

View File

@ -0,0 +1,6 @@
import { AppState } from "reducers";
import { User } from "constants/userConstants";
export const getCurrentUser = (state: AppState): User | undefined =>
state.ui.users.current;
export const getUsers = (state: AppState): User[] => state.ui.users.list;

18
app/client/src/store.ts Normal file
View File

@ -0,0 +1,18 @@
import { createStore, applyMiddleware } from "redux";
import {
useSelector as useReduxSelector,
TypedUseSelectorHook,
} from "react-redux";
import appReducer, { AppState } from "./reducers";
import createSagaMiddleware from "redux-saga";
import { rootSaga } from "./sagas";
import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
const sagaMiddleware = createSagaMiddleware();
export default createStore(
appReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware)),
);
sagaMiddleware.run(rootSaga);
export const useSelector: TypedUseSelectorHook<AppState> = useReduxSelector;

View File

@ -0,0 +1,42 @@
import React, { useState } from "react";
import TagInputComponent from "components/editorComponents/TagInputComponent";
import { withKnobs, text, select } from "@storybook/addon-knobs";
import { withDesign } from "storybook-addon-designs";
import { IntentColors } from "constants/DefaultTheme";
export default {
title: "TagListInput",
component: TagInputComponent,
decorators: [withKnobs, withDesign],
};
export const withDynamicProps = () =>
React.createElement(() => {
const [values, setValues] = useState(
"abhinav, test@appsmith.com, test2@appsmith.com",
);
return (
<TagInputComponent
placeholder={text("Placeholder", "Placeholder")}
input={{
value: values,
onChange: (value: string) => setValues(value),
}}
separator={text("Separator (string | RegExp)", ",")}
type="email"
intent={select("Intent", Object.keys(IntentColors), "success")}
/>
);
});
withDynamicProps.story = {
name: "Dynamic Props",
parameters: {
design: {
type: "figma",
url:
"https://www.figma.com/file/dcpKM4JTxsa7rd5MTcyJiT/Untitled?node-id=10%3A2",
},
},
};

View File

@ -25,3 +25,12 @@ export const getAbsolutePixels = (size?: string | null) => {
if (_dex === -1) return 0;
return parseInt(size.slice(0, _dex), 10);
};
export const Directions: { [id: string]: string } = {
UP: "up",
DOWN: "down",
LEFT: "left",
RIGHT: "right",
};
export type Direction = typeof Directions[keyof typeof Directions];

View File

@ -0,0 +1,27 @@
import localforage from "localforage";
import moment from "moment";
const STORAGE_KEYS: { [id: string]: string } = {
AUTH_EXPIRATION: "Auth.expiration",
};
const store = localforage.createInstance({
name: "Appsmith",
});
export const resetAuthExpiration = () => {
const expireBy = moment()
.add(1, "h")
.format();
store.setItem(STORAGE_KEYS.AUTH_EXPIRATION, expireBy).catch(error => {
console.log("Unable to set expiration time");
});
};
export const hasAuthExpired = async () => {
const expireBy: string = await store.getItem(STORAGE_KEYS.AUTH_EXPIRATION);
if (expireBy && moment().isAfter(moment(expireBy))) {
return true;
}
return false;
};

View File

@ -181,6 +181,8 @@ abstract class BaseWidget<
return <ErrorBoundary>{this.getPageView()}</ErrorBoundary>;
}
// TODO(Nikhil): Revisit the inclusion of another library for shallowEqual.
// `react-redux` provides shallowEqual natively
shouldComponentUpdate(nextProps: WidgetProps, nextState: WidgetState) {
const isNotEqual =
!shallowequal(nextProps, this.props) ||

View File

@ -1,11 +1,10 @@
import React from "react";
import _ from "lodash";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { ActionPayload, TableAction } from "constants/ActionConstants";
import { AutoResizer } from "react-base-table";
import "react-base-table/styles.css";
import { forIn } from "lodash";
import _, { forIn } from "lodash";
import SelectableTable, {
Column,
} from "components/designSystems/appsmith/TableComponent";

File diff suppressed because it is too large Load Diff