Feat/page not found (#37)

* Feat: Implement page not found UI

* fix: check user login status and minor code refactoring

 - Check if user is logged in or not in page header.
 - Based on login status show relevant CTAs
 - Fix ESLint errors
 - Move RoleNameCell and DeleteActionCell as seperate components.

* fix: Add catch all for pagenotfound

* fix: Use constants and update css syntax.

Co-authored-by: Arpit Mohan <me@arpitmohan.com>
This commit is contained in:
Tejaaswini Narendra 2020-07-08 15:44:03 +05:30 committed by GitHub
parent 5f39dfd88b
commit 7737b57667
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 220 additions and 147 deletions

View File

@ -5,7 +5,7 @@ import {
API_REQUEST_HEADERS,
} from "constants/ApiConstants";
import { ActionApiResponse } from "./ActionAPI";
import { AUTH_LOGIN_URL } from "constants/routes";
import { AUTH_LOGIN_URL, PAGE_NOT_FOUND_URL } from "constants/routes";
import { setRouteBeforeLogin } from "utils/storage";
import history from "utils/history";
@ -66,6 +66,17 @@ axiosInstance.interceptors.response.use(
});
}
}
if (
error.resonse.status === 404 &&
error.response.app_error_code === 4028
) {
history.push(PAGE_NOT_FOUND_URL);
return Promise.reject({
code: 404,
message: "Page Not Found",
show: false,
});
}
if (error.response.data.responseMeta) {
return Promise.resolve(error.response.data);
}

View File

@ -65,6 +65,7 @@ class UserApi extends Api {
static switchUserOrgURL = `${UserApi.usersURL}/switchOrganization`;
static addOrgURL = `${UserApi.usersURL}/addOrganization`;
static logoutURL = "v1/logout";
static currentUserURL = "v1/users/me";
static createUser(
request: CreateUserRequest,
@ -76,6 +77,10 @@ class UserApi extends Api {
return Api.get(UserApi.usersURL + "/" + request.id);
}
static getCurrentUser(): AxiosPromise<ApiResponse> {
return Api.get(UserApi.currentUserURL);
}
static forgotPassword(
request: ForgotPasswordRequest,
): AxiosPromise<ApiResponse> {

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

View File

@ -216,6 +216,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
GET_ALL_APPLICATION_INIT: "GET_ALL_APPLICATION_INIT",
FETCH_USER_APPLICATIONS_ORGS_SUCCESS: "FETCH_USER_APPLICATIONS_ORGS_SUCCESS",
FETCH_USER_DETAILS_SUCCESS: "FETCH_USER_DETAILS_SUCCESS",
FETCH_USER_DETAILS_ERROR: "FETCH_USER_DETAILS_ERROR",
FETCH_ALL_USERS_SUCCESS: "FETCH_ALL_USERS_SUCCESS",
FETCH_ALL_USERS_INIT: "FETCH_ALL_USERS_INIT",
FETCH_ALL_ROLES_SUCCESS: "FETCH_ALL_ROLES_SUCCESS",

View File

@ -2,6 +2,7 @@ import { MenuIcons } from "icons/MenuIcons";
export const BASE_URL = "/";
export const ORG_URL = "/org";
export const PAGE_NOT_FOUND_URL = "/404";
export const APPLICATIONS_URL = `/applications`;
export const BUILDER_URL = "/applications/:applicationId/pages/:pageId/edit";
export const USER_AUTH_URL = "/user";

View File

@ -1,3 +1,5 @@
export const ANONYMOUS_USERNAME = "anonymousUser";
export type User = {
id: string;
email: string;

View File

@ -23,6 +23,7 @@ import {
BASE_LOGIN_URL,
BASE_SIGNUP_URL,
USERS_URL,
PAGE_NOT_FOUND_URL,
} from "constants/routes";
import { LayersContext, Layers } from "constants/Layers";
@ -120,6 +121,11 @@ ReactDOM.render(
routeProtected
logDisable
/>
<AppRoute
path={PAGE_NOT_FOUND_URL}
component={PageNotFound}
name={"PageNotFound"}
/>
<AppRoute component={PageNotFound} name={"PageNotFound"} />
</Switch>
</Suspense>

View File

@ -12,7 +12,6 @@ import {
theme,
getBorderCSSShorthand,
getColorWithOpacity,
Theme,
} from "constants/DefaultTheme";
import ContextDropdown, {
ContextDropdownOption,

View File

@ -26,9 +26,7 @@ import { PERMISSION_TYPE, isPermitted } from "./permissionHelpers";
import { MenuIcons } from "icons/MenuIcons";
import { DELETING_APPLICATION } from "constants/messages";
import { AppToaster } from "components/editorComponents/ToastComponent";
import AnalyticsUtil from "utils/AnalyticsUtil";
import FormDialogComponent from "components/editorComponents/form/FormDialogComponent";
import OrganizationListMockResponse from "mockResponses/OrganisationListResponse";
import { User } from "constants/userConstants";
import CustomizedDropdown, {
CustomizedDropdownProps,
@ -107,7 +105,6 @@ const StyledDialog = styled(Dialog)<{ setMaxWidth?: boolean }>`
type ApplicationProps = {
applicationList: ApplicationPayload[];
fetchApplications: () => void;
createApplication: (appName: string) => void;
isCreatingApplication: boolean;
isFetchingApplications: boolean;
@ -319,8 +316,6 @@ const mapStateToProps = (state: AppState) => ({
});
const mapDispatchToProps = (dispatch: any) => ({
fetchApplications: () =>
dispatch({ type: ReduxActionTypes.FETCH_APPLICATION_LIST_INIT }),
getAllApplication: () =>
dispatch({ type: ReduxActionTypes.GET_ALL_APPLICATION_INIT }),
createApplication: (appName: string) => {

View File

@ -1,15 +1,18 @@
import React from "react";
import { useHistory, Link } from "react-router-dom";
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/HeaderDropdownData";
import { AppState } from "reducers";
import { Org } from "constants/orgConstants";
import { User } from "constants/userConstants";
import { User, ANONYMOUS_USERNAME } from "constants/userConstants";
import Logo from "assets/images/appsmith_logo.png";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { useEffect } from "react";
import { AUTH_LOGIN_URL, APPLICATIONS_URL } from "constants/routes";
import Button from "components/editorComponents/Button";
const StyledPageHeader = styled(StyledHeader)`
width: 100%;
@ -28,31 +31,47 @@ const LogoContainer = styled.div`
`;
type PageHeaderProps = {
orgs?: Org[];
currentOrg?: Org;
user?: User;
fetchCurrentUser: () => void;
};
export const PageHeader = (props: PageHeaderProps) => {
const { user } = props;
const { user, fetchCurrentUser } = props;
const history = useHistory();
useEffect(() => {
fetchCurrentUser();
}, [fetchCurrentUser]);
return (
<StyledPageHeader>
<LogoContainer>
<a href="/applications">
<Link to={APPLICATIONS_URL}>
<img className="logoimg" src={Logo} alt="Appsmith Logo" />
</a>
</Link>
</LogoContainer>
<StyledDropDownContainer>
{user && <CustomizedDropdown {...DropdownProps(user, user.username)} />}
{user && user.username !== ANONYMOUS_USERNAME ? (
<CustomizedDropdown {...DropdownProps(user, user.username)} />
) : (
<Button
filled
text="Sign In"
intent={"primary"}
size="small"
onClick={() => history.push(AUTH_LOGIN_URL)}
/>
)}
</StyledDropDownContainer>
</StyledPageHeader>
);
};
const mapStateToProps = (state: AppState) => ({
currentOrg: getCurrentOrg(state),
user: getCurrentUser(state),
orgs: getOrgs(state),
});
export default connect(mapStateToProps, null)(PageHeader);
const mapDispatchToProps = (dispatch: any) => ({
fetchCurrentUser: () => dispatch({ type: ReduxActionTypes.FETCH_USER_INIT }),
});
export default connect(mapStateToProps, mapDispatchToProps)(PageHeader);

View File

@ -1,45 +1,56 @@
import React from "react";
import styled from "styled-components";
import { NonIdealState, Button, Card, Elevation } from "@blueprintjs/core";
import { RouterProps } from "react-router";
import styled from "styled-components";
import Button from "components/editorComponents/Button";
import PageUnavailableImage from "assets/images/404-image.png";
import PageHeader from "pages/common/PageHeader";
import { BASE_URL } from "constants/routes";
const NotFoundPageWrapper = styled.div`
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
`;
const Title = styled.div`
font-size: ${props => props.theme.fontSizes[10]}px;
const Wrapper = styled.div`
text-align: center;
margin-top: 5%;
.bold-text {
font-weight: ${props => props.theme.fontWeights[3]};
font-size: 24px;
}
.page-unavailable-img {
width: 35%;
}
.button-position {
margin: auto;
}
`;
class PageNotFound extends React.PureComponent<RouterProps> {
public render() {
return (
<NotFoundPageWrapper>
<Card elevation={Elevation.TWO}>
<Title>
<span role="img" aria-label="Page Not Found">
🙊
</span>
</Title>
<NonIdealState
description={
"We didn't mean for you to reach this page. Let's find your way back to building awesome applications."
}
action={
<Button
onClick={() => {
this.props.history.push("/");
}}
>
{"Home"}
</Button>
}
/>
</Card>
</NotFoundPageWrapper>
<>
<PageHeader />
<Wrapper>
<img
src={PageUnavailableImage}
alt="Page Unavailable"
className="page-unavailable-img"
></img>
<div>
<p className="bold-text">Page not found</p>
<p>
Either this page doesn't exist, or you don't have access to <br />
this page.
</p>
<Button
filled
text="Go back to homepage"
intent="primary"
icon="arrow-right"
iconAlignment="right"
size="small"
className="button-position"
onClick={() => this.props.history.push(BASE_URL)}
/>
</div>
</Wrapper>
</>
);
}
}

View File

@ -23,6 +23,7 @@ import FormDialogComponent from "components/editorComponents/form/FormDialogComp
import { getCurrentUser } from "selectors/usersSelectors";
import { User } from "constants/userConstants";
import { useTable, useFlexLayout } from "react-table";
type OrgProps = {
allOrgs: Organization[];
changeOrgName: (value: string) => void;
@ -51,6 +52,8 @@ type DropdownProps = {
activeItem: string;
userRoles: object;
username: string;
changeOrgUserRole: (orgId: string, role: string, username: string) => void;
orgId: string;
};
const StyledDropDown = styled.div`
@ -79,6 +82,90 @@ const StyledMenu = styled(Menu)`
}
`;
const RoleNameCell = (props: any) => {
const {
roleName,
roles,
username,
isCurrentUser,
isChangingRole,
} = props.cellProps.row.original;
if (isCurrentUser) {
return <div>{roleName}</div>;
}
return (
<Popover
content={
<Dropdown
activeItem={roleName}
userRoles={roles}
username={username}
changeOrgUserRole={props.changeOrgUserRole}
orgId={props.orgId}
/>
}
position={Position.BOTTOM_LEFT}
>
<StyledDropDown>
{roleName}
<Icon icon="chevron-down" />
{isChangingRole ? <Spinner size={20} /> : undefined}
</StyledDropDown>
</Popover>
);
};
const DeleteActionCell = (props: any) => {
const { username, isCurrentUser, isDeleting } = props.cellProps.row.original;
return (
!isCurrentUser &&
(isDeleting ? (
<Spinner size={20} />
) : (
<FormIcons.DELETE_ICON
height={20}
width={20}
color={"grey"}
background={"grey"}
onClick={() => props.deleteOrgUser(props.orgId, username)}
style={{ alignSelf: "center", cursor: "pointer" }}
/>
))
);
};
const Dropdown = (props: DropdownProps) => {
return (
<StyledMenu>
{Object.entries(props.userRoles).map((role, index) => {
const MenuContent = (
<div>
<span>
<b>{role[0]}</b>
</span>
<div>{role[1]}</div>
</div>
);
return (
<MenuItem
multiline
key={index}
onClick={() =>
props.changeOrgUserRole(props.orgId, role[0], props.username)
}
active={props.activeItem === role[0]}
text={MenuContent}
/>
);
})}
</StyledMenu>
);
};
export const OrgSettings = (props: PageProps) => {
const {
match: {
@ -97,43 +184,7 @@ export const OrgSettings = (props: PageProps) => {
roles: props.allRole,
isCurrentUser: user.username === props.currentUser?.username,
}));
const data = React.useMemo(() => userTableData, [
props.allUsers,
props.allRole,
]);
const RoleNameCell = (cellProps: any) => {
const {
roleName,
roles,
username,
isCurrentUser,
isChangingRole,
} = cellProps.row.original;
if (isCurrentUser) {
return <div>{roleName}</div>;
}
return (
<Popover
content={
<Dropdown
activeItem={roleName}
userRoles={roles}
username={username}
/>
}
position={Position.BOTTOM_LEFT}
>
<StyledDropDown>
{roleName}
<Icon icon="chevron-down" />
{isChangingRole ? <Spinner size={20} /> : undefined}
</StyledDropDown>
</Popover>
);
};
const data = React.useMemo(() => userTableData, [userTableData]);
const columns = React.useMemo(() => {
return [
@ -148,37 +199,19 @@ export const OrgSettings = (props: PageProps) => {
{
Header: "Role",
accessor: "roleName",
Cell: RoleNameCell,
Cell: (cellProps: any) => {
return RoleNameCell({ cellProps, changeOrgUserRole, orgId });
},
},
{
Header: "Delete",
accessor: "delete",
Cell: (cellProps: any) => {
const {
username,
isCurrentUser,
isDeleting,
} = cellProps.row.original;
return (
!isCurrentUser &&
(isDeleting ? (
<Spinner size={20} />
) : (
<FormIcons.DELETE_ICON
height={20}
width={20}
color={"grey"}
background={"grey"}
onClick={() => deleteOrgUser(orgId, username)}
style={{ alignSelf: "center", cursor: "pointer" }}
/>
))
);
return DeleteActionCell({ cellProps, deleteOrgUser, orgId });
},
},
];
}, [props.allUsers, props.allRole]);
}, [orgId, deleteOrgUser, changeOrgUserRole]);
const currentOrg = allOrgs.find(org => org.organization.id === orgId);
const currentOrgName = currentOrg?.organization.name ?? "";
@ -203,33 +236,6 @@ export const OrgSettings = (props: PageProps) => {
getAllApplication();
}, [orgId, fetchUser, fetchAllRoles, getAllApplication]);
const Dropdown = (props: DropdownProps) => {
return (
<StyledMenu>
{Object.entries(props.userRoles).map((role, index) => {
const MenuContent = (
<div>
<span>
<b>{role[0]}</b>
</span>
<div>{role[1]}</div>
</div>
);
return (
<MenuItem
multiline
key={index}
onClick={() => changeOrgUserRole(orgId, role[0], props.username)}
active={props.activeItem === role[0]}
text={MenuContent}
/>
);
})}
</StyledMenu>
);
};
return (
<React.Fragment>
<PageSectionHeader>

View File

@ -7,7 +7,6 @@ import {
} from "constants/ReduxActionConstants";
import { Organization } from "constants/orgConstants";
import { ERROR_MESSAGE_CREATE_APPLICATION } from "constants/messages";
import { getApplicationPayload } from "mockComponentProps/ApplicationPayloads";
const initialState: ApplicationsReduxState = {
isFetchingApplications: false,

View File

@ -104,7 +104,7 @@ const orgReducer = createReducer(initialState, {
action: ReduxAction<{ username: string }>,
) => {
const _orgUsers = state.orgUsers.map((user: OrgUser) => {
if (user.username == action.payload.username) {
if (user.username === action.payload.username) {
return {
...user,
isChangingRole: true,
@ -119,7 +119,7 @@ const orgReducer = createReducer(initialState, {
action: ReduxAction<{ username: string }>,
) => {
const _orgUsers = state.orgUsers.map((user: OrgUser) => {
if (user.username == action.payload.username) {
if (user.username === action.payload.username) {
return {
...user,
isDeleting: true,

View File

@ -83,10 +83,6 @@ export function* getAllApplicationSaga() {
type: ReduxActionTypes.FETCH_USER_APPLICATIONS_ORGS_SUCCESS,
payload: organizationApplication,
});
yield put({
type: ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
payload: response.data.user,
});
}
} catch (error) {
yield put({

View File

@ -72,6 +72,27 @@ export function* createUserSaga(
}
}
export function* getCurrentUserSaga() {
try {
const response: ApiResponse = yield call(UserApi.getCurrentUser);
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.FETCH_USER_DETAILS_ERROR,
payload: {
error,
},
});
}
}
export function* forgotPasswordSaga(
action: ReduxActionWithPromise<ForgotPasswordRequest>,
) {
@ -326,6 +347,7 @@ export function* logoutSaga() {
export default function* userSagas() {
yield all([
takeLatest(ReduxActionTypes.CREATE_USER_INIT, createUserSaga),
takeLatest(ReduxActionTypes.FETCH_USER_INIT, getCurrentUserSaga),
takeLatest(ReduxActionTypes.FORGOT_PASSWORD_INIT, forgotPasswordSaga),
takeLatest(ReduxActionTypes.RESET_USER_PASSWORD_INIT, resetPasswordSaga),
takeLatest(