Add app header (#3223)

This commit is contained in:
Krzysztof Żuraw 2023-02-28 16:19:56 +01:00 committed by GitHub
parent 81881c4a47
commit c741ba3bf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 243 additions and 114 deletions

View file

@ -2625,6 +2625,9 @@
"context": "search", "context": "search",
"string": "No results" "string": "No results"
}, },
"HqRNN8": {
"string": "Support"
},
"HqeqEV": { "HqeqEV": {
"context": "create gift card product alert message", "context": "create gift card product alert message",
"string": "Create a gift card product" "string": "Create a gift card product"
@ -7202,6 +7205,9 @@
"rwOx2s": { "rwOx2s": {
"string": "Please provide a transaction reference using the input below:" "string": "Please provide a transaction reference using the input below:"
}, },
"rxNddi": {
"string": "Homepage"
},
"ryAyPr": { "ryAyPr": {
"string": "Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!" "string": "Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!"
}, },

View file

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

View 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>
);
};

View 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>
);

View 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>
);

View file

@ -0,0 +1,3 @@
export * from "./TopNav";
export * from "./TopNavLink";
export * from "./TopNavWrapper";

View file

@ -1,2 +1,3 @@
export { default } from "./AppLayout";
export * from "./AppLayout"; export * from "./AppLayout";
export { default } from "./AppLayout";
export * from "./TopNav";

View file

@ -32,6 +32,10 @@ const useStyles = makeStyles(
{ name: "Link" }, { name: "Link" },
); );
export interface LinkState {
from?: string;
}
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> { interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href?: string; href?: string;
color?: "primary" | "secondary"; color?: "primary" | "secondary";
@ -40,6 +44,7 @@ interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
typographyProps?: TypographyProps; typographyProps?: TypographyProps;
onClick?: React.MouseEventHandler; onClick?: React.MouseEventHandler;
disabled?: boolean; disabled?: boolean;
state?: LinkState;
} }
const Link: React.FC<LinkProps> = props => { const Link: React.FC<LinkProps> = props => {
@ -86,10 +91,24 @@ const Link: React.FC<LinkProps> = props => {
...linkProps, ...linkProps,
}; };
const urlObject = new URL(href, window.location.origin);
return ( return (
<> <>
{!!href && !isExternalURL(href) ? ( {!!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} {children}
</RouterLink> </RouterLink>
) : ( ) : (

View file

@ -2,14 +2,11 @@ import { AppLogo } from "@dashboard/new-apps/types";
import { Box, GenericAppIcon } from "@saleor/macaw-ui/next"; import { Box, GenericAppIcon } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
interface AppAvatarProps { const avatarSize = 11;
logo: AppLogo | undefined;
size: "medium" | "large";
}
export const AppAvatar: React.FC<AppAvatarProps> = ({ logo, size }) => {
const avatarSize = size === "medium" ? 11 : 13;
export const AppAvatar: React.FC<{
logo?: AppLogo | undefined;
}> = ({ logo }) => {
if (logo?.source) { if (logo?.source) {
return ( return (
<Box <Box
@ -27,8 +24,8 @@ export const AppAvatar: React.FC<AppAvatarProps> = ({ logo, size }) => {
} else { } else {
return ( return (
<Box <Box
__backgroundColor={logo?.color ?? "#EAE8E9"} __backgroundColor={logo?.color}
__color="#7C7F7F" backgroundColor="surfaceNeutralSubdued"
padding={3} padding={3}
width={avatarSize} width={avatarSize}
height={avatarSize} height={avatarSize}
@ -36,7 +33,7 @@ export const AppAvatar: React.FC<AppAvatarProps> = ({ logo, size }) => {
placeItems="center" placeItems="center"
borderRadius={2} borderRadius={2}
> >
<GenericAppIcon /> <GenericAppIcon size="large" color="iconNeutralSubdued" />
</Box> </Box>
); );
} }

View file

@ -2,6 +2,8 @@ import { GetV2SaleorAppsResponse } from "@dashboard/new-apps/marketplace.types";
import { Box, Text } from "@saleor/macaw-ui/next"; import { Box, Text } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { AppLogo } from "./AppLogo";
interface AppListCardDescriptionProps { interface AppListCardDescriptionProps {
app: GetV2SaleorAppsResponse.SaleorApp; app: GetV2SaleorAppsResponse.SaleorApp;
} }
@ -17,19 +19,10 @@ const AppListCardDescription: React.FC<AppListCardDescriptionProps> = ({
marginBottom={8} marginBottom={8}
gap={6} gap={6}
> >
<Box <AppLogo backgroundColor={app.logo.color}>
width={13} {app.logo.source ? (
height={13} <img src={app.logo.source} alt="App logo" />
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 && (
<Text <Text
variant="bodyEmp" variant="bodyEmp"
size="large" size="large"
@ -37,10 +30,10 @@ const AppListCardDescription: React.FC<AppListCardDescriptionProps> = ({
data-test-id="app-logo-placeholder" data-test-id="app-logo-placeholder"
color="textNeutralContrasted" color="textNeutralContrasted"
> >
{app.name.en[0] || ""} {app.name.en.charAt(0).toUpperCase() || ""}
</Text> </Text>
)} )}
</Box> </AppLogo>
<Text variant="bodyStrong" size="medium" color="textNeutralDefault"> <Text variant="bodyStrong" size="medium" color="textNeutralDefault">
<strong>{app.name.en}</strong> <strong>{app.name.en}</strong>
</Text> </Text>

View 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>
);

View file

@ -1,2 +1,3 @@
export * from "./AppListCard"; export * from "./AppListCard";
export { default } from "./AppListCard"; export { default } from "./AppListCard";
export * from "./AppLogo";

View file

@ -1,7 +1,9 @@
import { DetailPageLayout } from "@dashboard/components/Layouts";
import { AppQuery } from "@dashboard/graphql"; import { AppQuery } from "@dashboard/graphql";
import React from "react"; import React from "react";
import { AppFrame } from "../AppFrame"; import { AppFrame } from "../AppFrame";
import { AppPageNav } from "./AppPageNav";
import { useStyles } from "./styles"; import { useStyles } from "./styles";
export interface AppPageProps { export interface AppPageProps {
@ -20,17 +22,25 @@ export const AppPage: React.FC<AppPageProps> = ({
const classes = useStyles(); const classes = useStyles();
return ( return (
<DetailPageLayout gridTemplateColumns={1}>
<AppPageNav
name={data?.name}
supportUrl={data?.supportUrl}
homepageUrl={data?.homepageUrl}
/>
<div className={classes.iframeContainer}> <div className={classes.iframeContainer}>
{url && data.id && data.accessToken && ( {url && data?.id && data?.accessToken && (
<AppFrame <AppFrame
src={url} src={url}
appToken={data?.accessToken} appToken={data?.accessToken ?? ""}
onError={onError} onError={onError}
appId={data?.id} appId={data?.id ?? ""}
refetch={refetch} refetch={refetch}
/> />
)} )}
</div> </div>
</DetailPageLayout>
); );
}; };

View 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>
);
};

View file

@ -15,6 +15,7 @@ import {
} from "@saleor/macaw-ui/next"; } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useLocation } from "react-router";
import { AppAvatar } from "../AppAvatar/AppAvatar"; import { AppAvatar } from "../AppAvatar/AppAvatar";
import AppPermissions from "../AppPermissions"; import AppPermissions from "../AppPermissions";
@ -24,10 +25,12 @@ export const InstalledAppListRow: React.FC<InstalledApp> = props => {
const { app, isExternal, logo } = props; const { app, isExternal, logo } = props;
const intl = useIntl(); const intl = useIntl();
const { openAppSettings } = useAppListContext(); const { openAppSettings } = useAppListContext();
const location = useLocation();
return ( return (
<Link <Link
href={AppUrls.resolveAppUrl(app.id)} href={AppUrls.resolveAppUrl(app.id)}
state={{ from: location.pathname }}
className={sprinkles({ display: "contents" })} className={sprinkles({ display: "contents" })}
inline={false} inline={false}
> >
@ -46,7 +49,7 @@ export const InstalledAppListRow: React.FC<InstalledApp> = props => {
gap={5} gap={5}
alignItems="center" alignItems="center"
> >
<AppAvatar size="medium" logo={logo} /> <AppAvatar logo={logo} />
<Text variant="bodyStrong">{app.name}</Text> <Text variant="bodyStrong">{app.name}</Text>
<Text variant="body" color="textNeutralSubdued"> <Text variant="body" color="textNeutralSubdued">
{`v${app.version}`} {`v${app.version}`}

View file

@ -46,7 +46,7 @@ export const NotInstalledAppListRow: React.FC<AppInstallation> = props => {
alignItems="center" alignItems="center"
justifyContent={{ mobile: "space-between", desktop: "flex-start" }} justifyContent={{ mobile: "space-between", desktop: "flex-start" }}
> >
<AppAvatar size="medium" logo={logo} /> <AppAvatar logo={logo} />
<Text variant="bodyStrong">{appInstallation.appName}</Text> <Text variant="bodyStrong">{appInstallation.appName}</Text>
{isExternal && ( {isExternal && (
<Chip data-test-id="app-external-label" size="large"> <Chip data-test-id="app-external-label" size="large">