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",
"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!"
},

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 { default } from "./AppLayout";
export * from "./TopNav";

View file

@ -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>
) : (

View file

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

View file

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

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 { default } from "./AppListCard";
export * from "./AppLogo";

View file

@ -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 (
<DetailPageLayout gridTemplateColumns={1}>
<AppPageNav
name={data?.name}
supportUrl={data?.supportUrl}
homepageUrl={data?.homepageUrl}
/>
<div className={classes.iframeContainer}>
{url && data.id && data.accessToken && (
{url && data?.id && data?.accessToken && (
<AppFrame
src={url}
appToken={data?.accessToken}
appToken={data?.accessToken ?? ""}
onError={onError}
appId={data?.id}
appId={data?.id ?? ""}
refetch={refetch}
/>
)}
</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";
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}`}

View file

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