Add app header (#3223)
This commit is contained in:
parent
81881c4a47
commit
c741ba3bf1
16 changed files with 243 additions and 114 deletions
|
@ -2625,6 +2625,9 @@
|
|||
"context": "search",
|
||||
"string": "No results"
|
||||
},
|
||||
"HqRNN8": {
|
||||
"string": "Support"
|
||||
},
|
||||
"HqeqEV": {
|
||||
"context": "create gift card product alert message",
|
||||
"string": "Create a gift card product"
|
||||
|
@ -7202,6 +7205,9 @@
|
|||
"rwOx2s": {
|
||||
"string": "Please provide a transaction reference using the input below:"
|
||||
},
|
||||
"rxNddi": {
|
||||
"string": "Homepage"
|
||||
},
|
||||
"ryAyPr": {
|
||||
"string": "Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!"
|
||||
},
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
import { ArrowLeftIcon, Box, sprinkles, Text } from "@saleor/macaw-ui/next";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import useAppChannel from "./AppChannelContext";
|
||||
import AppChannelSelect from "./AppChannelSelect";
|
||||
import { topBarHeight } from "./consts";
|
||||
|
||||
interface TopNavProps {
|
||||
title: string | React.ReactNode;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||
title,
|
||||
href,
|
||||
children,
|
||||
}) => {
|
||||
const { availableChannels, channel, isPickerActive, setChannel } =
|
||||
useAppChannel(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
paddingX={9}
|
||||
paddingRight={9}
|
||||
paddingY={8}
|
||||
borderBottomWidth={1}
|
||||
borderBottomStyle="solid"
|
||||
borderColor="neutralPlain"
|
||||
position="relative"
|
||||
data-test-id="page-header"
|
||||
__height={topBarHeight}
|
||||
gridColumn="8"
|
||||
gridRowStart="1"
|
||||
backgroundColor="plain"
|
||||
>
|
||||
{href && (
|
||||
<Link
|
||||
to={href}
|
||||
data-test-id="app-header-back-button"
|
||||
className={sprinkles({
|
||||
borderColor: "neutralPlain",
|
||||
borderStyle: "solid",
|
||||
borderWidth: 1,
|
||||
padding: 5,
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginRight: 7,
|
||||
})}
|
||||
>
|
||||
<ArrowLeftIcon />
|
||||
</Link>
|
||||
)}
|
||||
<Box __flex={1}>
|
||||
<Text variant="title">{title}</Text>
|
||||
</Box>
|
||||
<Box display="flex" flexWrap="nowrap">
|
||||
{isPickerActive && (
|
||||
<AppChannelSelect
|
||||
channels={availableChannels}
|
||||
selectedChannelId={channel?.id}
|
||||
onChannelSelect={setChannel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
40
src/components/AppLayout/TopNav/TopNav.tsx
Normal file
40
src/components/AppLayout/TopNav/TopNav.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
import useAppChannel from "../AppChannelContext";
|
||||
import AppChannelSelect from "../AppChannelSelect";
|
||||
import { TopNavLink } from "./TopNavLink";
|
||||
import { TopNavWrapper } from "./TopNavWrapper";
|
||||
|
||||
interface TopNavProps {
|
||||
title: string | React.ReactNode;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||
title,
|
||||
href,
|
||||
children,
|
||||
}) => {
|
||||
const { availableChannels, channel, isPickerActive, setChannel } =
|
||||
useAppChannel(false);
|
||||
|
||||
return (
|
||||
<TopNavWrapper>
|
||||
{href && <TopNavLink to={href} />}
|
||||
<Box __flex={1} marginLeft={5}>
|
||||
<Text variant="title">{title}</Text>
|
||||
</Box>
|
||||
<Box display="flex" flexWrap="nowrap">
|
||||
{isPickerActive && (
|
||||
<AppChannelSelect
|
||||
channels={availableChannels}
|
||||
selectedChannelId={channel?.id}
|
||||
onChannelSelect={setChannel}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
</TopNavWrapper>
|
||||
);
|
||||
};
|
17
src/components/AppLayout/TopNav/TopNavLink.tsx
Normal file
17
src/components/AppLayout/TopNav/TopNavLink.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { ArrowLeftIcon, Button } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const TopNavLink: React.FC<{
|
||||
to: string;
|
||||
variant?: "secondary" | "tertiary";
|
||||
}> = ({ to, variant = "secondary" }) => (
|
||||
<Link to={to}>
|
||||
<Button
|
||||
icon={<ArrowLeftIcon />}
|
||||
variant={variant}
|
||||
size="large"
|
||||
data-test-id="app-header-back-button"
|
||||
/>
|
||||
</Link>
|
||||
);
|
24
src/components/AppLayout/TopNav/TopNavWrapper.tsx
Normal file
24
src/components/AppLayout/TopNav/TopNavWrapper.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
import { topBarHeight } from "../consts";
|
||||
|
||||
export const TopNavWrapper: React.FC = ({ children }) => (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
paddingX={9}
|
||||
paddingY={8}
|
||||
borderBottomWidth={1}
|
||||
borderBottomStyle="solid"
|
||||
borderColor="neutralPlain"
|
||||
position="relative"
|
||||
data-test-id="page-header"
|
||||
__height={topBarHeight}
|
||||
gridColumn="8"
|
||||
gridRowStart="1"
|
||||
backgroundColor="plain"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
3
src/components/AppLayout/TopNav/index.ts
Normal file
3
src/components/AppLayout/TopNav/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "./TopNav";
|
||||
export * from "./TopNavLink";
|
||||
export * from "./TopNavWrapper";
|
|
@ -1,2 +1,3 @@
|
|||
export { default } from "./AppLayout";
|
||||
export * from "./AppLayout";
|
||||
export { default } from "./AppLayout";
|
||||
export * from "./TopNav";
|
||||
|
|
|
@ -32,6 +32,10 @@ const useStyles = makeStyles(
|
|||
{ name: "Link" },
|
||||
);
|
||||
|
||||
export interface LinkState {
|
||||
from?: string;
|
||||
}
|
||||
|
||||
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
href?: string;
|
||||
color?: "primary" | "secondary";
|
||||
|
@ -40,6 +44,7 @@ interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|||
typographyProps?: TypographyProps;
|
||||
onClick?: React.MouseEventHandler;
|
||||
disabled?: boolean;
|
||||
state?: LinkState;
|
||||
}
|
||||
|
||||
const Link: React.FC<LinkProps> = props => {
|
||||
|
@ -86,10 +91,24 @@ const Link: React.FC<LinkProps> = props => {
|
|||
...linkProps,
|
||||
};
|
||||
|
||||
const urlObject = new URL(href, window.location.origin);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!href && !isExternalURL(href) ? (
|
||||
<RouterLink to={disabled ? undefined : href} {...commonLinkProps}>
|
||||
<RouterLink<LinkState>
|
||||
to={
|
||||
disabled
|
||||
? undefined
|
||||
: {
|
||||
pathname: urlObject.pathname,
|
||||
search: urlObject.search,
|
||||
hash: urlObject.hash,
|
||||
state: props.state,
|
||||
}
|
||||
}
|
||||
{...commonLinkProps}
|
||||
>
|
||||
{children}
|
||||
</RouterLink>
|
||||
) : (
|
||||
|
|
|
@ -2,14 +2,11 @@ import { AppLogo } from "@dashboard/new-apps/types";
|
|||
import { Box, GenericAppIcon } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
interface AppAvatarProps {
|
||||
logo: AppLogo | undefined;
|
||||
size: "medium" | "large";
|
||||
}
|
||||
|
||||
export const AppAvatar: React.FC<AppAvatarProps> = ({ logo, size }) => {
|
||||
const avatarSize = size === "medium" ? 11 : 13;
|
||||
const avatarSize = 11;
|
||||
|
||||
export const AppAvatar: React.FC<{
|
||||
logo?: AppLogo | undefined;
|
||||
}> = ({ logo }) => {
|
||||
if (logo?.source) {
|
||||
return (
|
||||
<Box
|
||||
|
@ -27,8 +24,8 @@ export const AppAvatar: React.FC<AppAvatarProps> = ({ logo, size }) => {
|
|||
} else {
|
||||
return (
|
||||
<Box
|
||||
__backgroundColor={logo?.color ?? "#EAE8E9"}
|
||||
__color="#7C7F7F"
|
||||
__backgroundColor={logo?.color}
|
||||
backgroundColor="surfaceNeutralSubdued"
|
||||
padding={3}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
|
@ -36,7 +33,7 @@ export const AppAvatar: React.FC<AppAvatarProps> = ({ logo, size }) => {
|
|||
placeItems="center"
|
||||
borderRadius={2}
|
||||
>
|
||||
<GenericAppIcon />
|
||||
<GenericAppIcon size="large" color="iconNeutralSubdued" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { GetV2SaleorAppsResponse } from "@dashboard/new-apps/marketplace.types";
|
|||
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
import { AppLogo } from "./AppLogo";
|
||||
|
||||
interface AppListCardDescriptionProps {
|
||||
app: GetV2SaleorAppsResponse.SaleorApp;
|
||||
}
|
||||
|
@ -17,19 +19,10 @@ const AppListCardDescription: React.FC<AppListCardDescriptionProps> = ({
|
|||
marginBottom={8}
|
||||
gap={6}
|
||||
>
|
||||
<Box
|
||||
width={13}
|
||||
height={13}
|
||||
display="flex"
|
||||
placeItems="center"
|
||||
borderRadius={3}
|
||||
style={{
|
||||
backgroundColor: app.logo.color,
|
||||
}}
|
||||
data-test-id="app-logo"
|
||||
>
|
||||
{app.logo.source && <img src={app.logo.source} alt="App logo" />}
|
||||
{!app.logo.source && (
|
||||
<AppLogo backgroundColor={app.logo.color}>
|
||||
{app.logo.source ? (
|
||||
<img src={app.logo.source} alt="App logo" />
|
||||
) : (
|
||||
<Text
|
||||
variant="bodyEmp"
|
||||
size="large"
|
||||
|
@ -37,10 +30,10 @@ const AppListCardDescription: React.FC<AppListCardDescriptionProps> = ({
|
|||
data-test-id="app-logo-placeholder"
|
||||
color="textNeutralContrasted"
|
||||
>
|
||||
{app.name.en[0] || ""}
|
||||
{app.name.en.charAt(0).toUpperCase() || ""}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</AppLogo>
|
||||
<Text variant="bodyStrong" size="medium" color="textNeutralDefault">
|
||||
<strong>{app.name.en}</strong>
|
||||
</Text>
|
||||
|
|
23
src/new-apps/components/AppListCard/AppLogo.tsx
Normal file
23
src/new-apps/components/AppListCard/AppLogo.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
interface AppLogoProps {
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
export const AppLogo: React.FC<AppLogoProps> = ({
|
||||
backgroundColor,
|
||||
children,
|
||||
}) => (
|
||||
<Box
|
||||
width={13}
|
||||
height={13}
|
||||
display="flex"
|
||||
placeItems="center"
|
||||
borderRadius={3}
|
||||
data-test-id="app-logo"
|
||||
__backgroundColor={backgroundColor}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./AppListCard";
|
||||
export { default } from "./AppListCard";
|
||||
export * from "./AppLogo";
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import { AppQuery } from "@dashboard/graphql";
|
||||
import React from "react";
|
||||
|
||||
import { AppFrame } from "../AppFrame";
|
||||
import { AppPageNav } from "./AppPageNav";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
export interface AppPageProps {
|
||||
|
@ -20,17 +22,25 @@ export const AppPage: React.FC<AppPageProps> = ({
|
|||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.iframeContainer}>
|
||||
{url && data.id && data.accessToken && (
|
||||
<AppFrame
|
||||
src={url}
|
||||
appToken={data?.accessToken}
|
||||
onError={onError}
|
||||
appId={data?.id}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DetailPageLayout gridTemplateColumns={1}>
|
||||
<AppPageNav
|
||||
name={data?.name}
|
||||
supportUrl={data?.supportUrl}
|
||||
homepageUrl={data?.homepageUrl}
|
||||
/>
|
||||
|
||||
<div className={classes.iframeContainer}>
|
||||
{url && data?.id && data?.accessToken && (
|
||||
<AppFrame
|
||||
src={url}
|
||||
appToken={data?.accessToken ?? ""}
|
||||
onError={onError}
|
||||
appId={data?.id ?? ""}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DetailPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
66
src/new-apps/components/AppPage/AppPageNav.tsx
Normal file
66
src/new-apps/components/AppPage/AppPageNav.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { appsListUrl } from "@dashboard/apps/urls";
|
||||
import { TopNavLink, TopNavWrapper } from "@dashboard/components/AppLayout";
|
||||
import { LinkState } from "@dashboard/components/Link";
|
||||
import { AppAvatar } from "@dashboard/new-apps/components/AppAvatar/AppAvatar";
|
||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
interface AppPageNavProps {
|
||||
name: string | undefined | null;
|
||||
supportUrl: string | undefined | null;
|
||||
homepageUrl: string | undefined | null;
|
||||
}
|
||||
|
||||
export const AppPageNav: React.FC<AppPageNavProps> = ({
|
||||
name,
|
||||
supportUrl,
|
||||
homepageUrl,
|
||||
}) => {
|
||||
const location = useLocation<LinkState>();
|
||||
const goBackLink = location.state?.from ?? appsListUrl();
|
||||
|
||||
return (
|
||||
<TopNavWrapper>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<Box display="flex" gap={7} alignItems="center">
|
||||
<TopNavLink to={goBackLink} variant="tertiary" />
|
||||
<Box display="flex" gap={5} alignItems="center">
|
||||
<AppAvatar />
|
||||
<Text variant="heading">{name}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box display="flex" gap={4}>
|
||||
{supportUrl && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
window.open(supportUrl, "_blank");
|
||||
}}
|
||||
>
|
||||
<FormattedMessage defaultMessage="Support" id="HqRNN8" />
|
||||
</Button>
|
||||
)}
|
||||
{homepageUrl && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
window.open(homepageUrl, "_blank");
|
||||
}}
|
||||
>
|
||||
<FormattedMessage defaultMessage="Homepage" id="rxNddi" />
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</TopNavWrapper>
|
||||
);
|
||||
};
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
import { AppAvatar } from "../AppAvatar/AppAvatar";
|
||||
import AppPermissions from "../AppPermissions";
|
||||
|
@ -24,10 +25,12 @@ export const InstalledAppListRow: React.FC<InstalledApp> = props => {
|
|||
const { app, isExternal, logo } = props;
|
||||
const intl = useIntl();
|
||||
const { openAppSettings } = useAppListContext();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={AppUrls.resolveAppUrl(app.id)}
|
||||
state={{ from: location.pathname }}
|
||||
className={sprinkles({ display: "contents" })}
|
||||
inline={false}
|
||||
>
|
||||
|
@ -46,7 +49,7 @@ export const InstalledAppListRow: React.FC<InstalledApp> = props => {
|
|||
gap={5}
|
||||
alignItems="center"
|
||||
>
|
||||
<AppAvatar size="medium" logo={logo} />
|
||||
<AppAvatar logo={logo} />
|
||||
<Text variant="bodyStrong">{app.name}</Text>
|
||||
<Text variant="body" color="textNeutralSubdued">
|
||||
{`v${app.version}`}
|
||||
|
|
|
@ -46,7 +46,7 @@ export const NotInstalledAppListRow: React.FC<AppInstallation> = props => {
|
|||
alignItems="center"
|
||||
justifyContent={{ mobile: "space-between", desktop: "flex-start" }}
|
||||
>
|
||||
<AppAvatar size="medium" logo={logo} />
|
||||
<AppAvatar logo={logo} />
|
||||
<Text variant="bodyStrong">{appInstallation.appName}</Text>
|
||||
{isExternal && (
|
||||
<Chip data-test-id="app-external-label" size="large">
|
||||
|
|
Loading…
Reference in a new issue