Update Feed App UI (#323)

* Update the Feed App UI
This commit is contained in:
Krzysztof Wolski 2023-03-24 14:45:22 +01:00 committed by GitHub
parent 2f94183dc6
commit 5fad97c6d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 214 additions and 114 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-products-feed": patch
---
Update the UI to the common theme

View file

@ -1,22 +1,22 @@
import { trpcClient } from "../../trpc/trpc-client"; import { trpcClient } from "../../trpc/trpc-client";
import { LinearProgress, Paper } from "@material-ui/core"; import { LinearProgress, Paper } from "@material-ui/core";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { makeStyles } from "@saleor/macaw-ui"; import { EditIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import { AppConfigContainer } from "../app-config-container"; import { AppConfigContainer } from "../app-config-container";
import { UrlConfigurationForm } from "./url-configuration-form"; import { UrlConfigurationForm } from "./url-configuration-form";
import { ChannelsList } from "./channels-list";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge"; import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { AppColumnsLayout } from "../../ui/app-columns-layout"; import { AppColumnsLayout } from "../../ui/app-columns-layout";
import { FeedPreviewCard } from "./feed-preview-card"; import { FeedPreviewCard } from "./feed-preview-card";
import { Instructions } from "./instructions"; import { Instructions } from "./instructions";
import SideMenu from "./side-menu";
const useStyles = makeStyles((theme) => { const useStyles = makeStyles((theme) => {
return { return {
header: { marginBottom: 20 }, grid: {
grid: { display: "grid", gridTemplateColumns: "1fr 1fr", alignItems: "start", gap: 40 }, display: "grid",
formContainer: { gridTemplateColumns: "1fr 1fr",
top: 0, alignItems: "start",
position: "sticky", gap: 40,
}, },
instructionsContainer: { instructionsContainer: {
padding: 15, padding: 15,
@ -78,16 +78,30 @@ export const ChannelsConfiguration = () => {
return ( return (
<AppColumnsLayout> <AppColumnsLayout>
<ChannelsList <SideMenu
channels={channels.data} title="Channels"
activeChannelSlug={activeChannel.slug} selectedItemId={activeChannel?.slug}
onChannelClick={setActiveChannelSlug} headerToolbar={
<IconButton
variant="secondary"
onClick={() => {
appBridge?.dispatch(
actions.Redirect({
to: `/channels/`,
})
);
}}
>
<EditIcon />
</IconButton>
}
onClick={(id) => setActiveChannelSlug(id)}
items={channels.data.map((c) => ({ label: c.name, id: c.slug })) || []}
/> />
{activeChannel ? ( {activeChannel ? (
<div className={styles.configurationColumn}> <div className={styles.configurationColumn}>
<FeedPreviewCard channelSlug={activeChannel.slug} /> <Paper elevation={0}>
<Paper elevation={0} className={styles.formContainer}>
<UrlConfigurationForm <UrlConfigurationForm
channelID={activeChannel.id} channelID={activeChannel.id}
key={activeChannelSlug} key={activeChannelSlug}
@ -106,6 +120,7 @@ export const ChannelsConfiguration = () => {
/> />
{saveError && <span>{saveError.message}</span>} {saveError && <span>{saveError.message}</span>}
</Paper> </Paper>
<FeedPreviewCard channelSlug={activeChannel.slug} />
</div> </div>
) : null} ) : null}
<Instructions /> <Instructions />

View file

@ -1,68 +0,0 @@
import {
makeStyles,
OffsettedList,
OffsettedListBody,
OffsettedListHeader,
OffsettedListItem,
OffsettedListItemCell,
} from "@saleor/macaw-ui";
import clsx from "clsx";
import { Typography } from "@material-ui/core";
import React from "react";
import { ChannelFragment } from "../../../../generated/graphql";
const useStyles = makeStyles((theme) => {
return {
listItem: {
cursor: "pointer",
height: "auto !important",
border: `1px solid transparent`,
},
listItemActive: {
border: `1px solid ${
theme.palette.type === "light" ? theme.palette.divider : theme.palette.grey.A200
}`,
},
cellSlug: {
fontFamily: "monospace",
opacity: 0.8,
},
};
});
type Props = {
channels: ChannelFragment[];
activeChannelSlug: string;
onChannelClick(channelSlug: string): void;
};
export const ChannelsList = ({ channels, activeChannelSlug, onChannelClick }: Props) => {
const styles = useStyles();
return (
<OffsettedList gridTemplate={["1fr"]}>
<OffsettedListBody>
{channels.map((c) => {
return (
<OffsettedListItem
className={clsx(styles.listItem, {
[styles.listItemActive]: c.slug === activeChannelSlug,
})}
key={c.slug}
onClick={() => {
onChannelClick(c.slug);
}}
>
<OffsettedListItemCell>
{c.name}
<Typography variant="caption">
<code>{c.slug}</code>
</Typography>
</OffsettedListItemCell>
</OffsettedListItem>
);
})}
</OffsettedListBody>
</OffsettedList>
);
};

View file

@ -38,8 +38,8 @@ export const FeedPreviewCard = ({ channelSlug }: FeedPreviewCardProps) => {
}; };
return ( return (
<Paper elevation={0} className={styles.instructionsContainer}> <Paper elevation={0} className={styles.instructionsContainer}>
<Typography paragraph variant="h4"> <Typography paragraph variant="h3">
Your Google Merchant Feed Your Google Merchant Feed preview
</Typography> </Typography>
<TextField <TextField
label="Google feed URL" label="Google feed URL"
@ -47,6 +47,7 @@ export const FeedPreviewCard = ({ channelSlug }: FeedPreviewCardProps) => {
value={googleFeedUrl} value={googleFeedUrl}
disabled={true} disabled={true}
className={styles.field} className={styles.field}
helperText="Dedicated URL for your Google Merchant Feed"
/> />
<Button <Button

View file

@ -0,0 +1,111 @@
import { Card, CardContent, CardHeader, Divider } from "@material-ui/core";
("@material-ui/icons");
import { DeleteIcon, IconButton, List, ListItem, ListItemCell } from "@saleor/macaw-ui";
import clsx from "clsx";
import React from "react";
import { makeStyles } from "@saleor/macaw-ui";
import { Skeleton } from "@material-ui/lab";
export const useStyles = makeStyles((theme) => ({
menu: {
height: "fit-content",
},
clickable: {
cursor: "pointer",
},
selected: {
"&&&&::before": {
position: "absolute",
left: 0,
width: "4px",
height: "100%",
backgroundColor: theme.palette.saleor.active[1],
},
},
spaceBetween: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
tableRow: {
minHeight: "48px",
"&::after": {
display: "none",
},
},
greyText: {
color: theme.palette.text.hint,
},
link: {
all: "inherit",
display: "contents",
},
}));
interface SideMenuProps {
title: string;
noItemsText?: string;
items: { id: string; label: string }[];
selectedItemId?: string;
headerToolbar?: React.ReactNode;
onDelete?: (itemId: string) => void;
onClick: (itemId: string) => void;
}
export const SideMenu: React.FC<SideMenuProps> = ({
title,
items,
headerToolbar,
selectedItemId,
noItemsText,
onDelete,
onClick,
}) => {
const classes = useStyles();
const isNoItems = !items || !items.length;
return (
<Card className={classes.menu}>
<CardHeader title={title} action={headerToolbar} />
{isNoItems ? (
!!noItemsText && <CardContent className={classes.greyText}>{noItemsText}</CardContent>
) : (
<List gridTemplate={["1fr"]}>
{items.map((item) => (
<React.Fragment key={item.id}>
<Divider />
<ListItem
className={clsx(classes.clickable, classes.tableRow, {
[classes.selected]: item.id === selectedItemId,
})}
onClick={() => onClick(item.id)}
>
<ListItemCell>
<div className={classes.spaceBetween}>
{item.label}
{!!onDelete && (
<IconButton
variant="secondary"
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
onDelete(item.id);
}}
>
<DeleteIcon />
</IconButton>
)}
</div>
</ListItemCell>
</ListItem>
</React.Fragment>
)) ?? <Skeleton />}
<Divider />
</List>
)}
</Card>
);
};
export default SideMenu;

View file

@ -53,12 +53,12 @@ export const UrlConfigurationForm = (props: Props) => {
})} })}
className={styles.form} className={styles.form}
> >
<Typography variant="body1" paragraph> <Typography variant="h3" paragraph>
Configure Configure
<strong onClick={handleChannelNameClick} className={styles.channelName}> <strong onClick={handleChannelNameClick} className={styles.channelName}>
{` ${props.channelName} `} {` ${props.channelName} `}
</strong> </strong>
channel: channel
</Typography> </Typography>
<TextField label="Storefront home URL" {...CommonFieldProps} {...register("storefrontUrl")} /> <TextField label="Storefront home URL" {...CommonFieldProps} {...register("storefrontUrl")} />
<TextField <TextField

View file

@ -1,20 +1,20 @@
import { makeStyles } from "@saleor/macaw-ui"; import { makeStyles } from "@saleor/macaw-ui";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
const useStyles = makeStyles({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: "grid", display: "grid",
gridTemplateColumns: "280px auto 400px", gridTemplateColumns: "280px auto 400px",
alignItems: "start", alignItems: "start",
gap: 32, gap: theme.spacing(3),
maxWidth: 1180, marginTop: theme.spacing(3),
margin: "0 auto", marginLeft: theme.spacing(3),
}, },
}); }));
type Props = PropsWithChildren<{}>; type AppColumnsLayoutProps = PropsWithChildren<{}>;
export const AppColumnsLayout = ({ children }: Props) => { export const AppColumnsLayout = ({ children }: AppColumnsLayoutProps) => {
const styles = useStyles(); const styles = useStyles();
return <div className={styles.root}>{children}</div>; return <div className={styles.root}>{children}</div>;

View file

@ -1,9 +1,14 @@
import "../styles/globals.css"; import "../styles/globals.css";
import { Theme } from "@material-ui/core/styles"; import { createGenerateClassName, Theme } from "@material-ui/core/styles";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next"; import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import { ThemeProvider as MacawUIThemeProvider } from "@saleor/macaw-ui"; import {
dark,
light,
SaleorThemeColors,
ThemeProvider as MacawUIThemeProvider,
} from "@saleor/macaw-ui";
import React, { PropsWithChildren, useEffect } from "react"; import React, { PropsWithChildren, useEffect } from "react";
import { AppProps } from "next/app"; import { AppProps } from "next/app";
@ -11,12 +16,49 @@ import { ThemeSynchronizer } from "../lib/theme-synchronizer";
import { NoSSRWrapper } from "../lib/no-ssr-wrapper"; import { NoSSRWrapper } from "../lib/no-ssr-wrapper";
import { trpcClient } from "../modules/trpc/trpc-client"; import { trpcClient } from "../modules/trpc/trpc-client";
const themeOverrides: Partial<Theme> = { type PalettesOverride = Record<"light" | "dark", SaleorThemeColors>;
/**
* You can override MacawUI theme here /**
* Temporary override of colors, to match new dashboard palette.
* Long term this will be replaced with Macaw UI 2.x with up to date design tokens
*/ */
const palettes: PalettesOverride = {
light: {
...light,
background: {
default: "#fff",
paper: "#fff",
},
},
dark: {
...dark,
background: {
default: "hsla(211, 42%, 14%, 1)",
paper: "hsla(211, 42%, 14%, 1)",
},
},
}; };
const themeOverrides: Partial<Theme> = {
overrides: {
MuiTableCell: {
body: {
paddingBottom: 8,
paddingTop: 8,
},
root: {
height: 56,
paddingBottom: 4,
paddingTop: 4,
},
},
},
};
const generateClassName = createGenerateClassName({
productionPrefix: "c",
disableGlobal: true,
});
/** /**
* Ensure instance is a singleton. * Ensure instance is a singleton.
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk * TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
@ -27,7 +69,7 @@ export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge()
* That's a hack required by Macaw-UI incompatibility with React@18 * That's a hack required by Macaw-UI incompatibility with React@18
*/ */
const ThemeProvider = MacawUIThemeProvider as React.FC< const ThemeProvider = MacawUIThemeProvider as React.FC<
PropsWithChildren<{ overrides?: Partial<Theme>; ssr: boolean }> PropsWithChildren<{ overrides?: Partial<Theme>; ssr: boolean; palettes: PalettesOverride }>
>; >;
function NextApp({ Component, pageProps }: AppProps) { function NextApp({ Component, pageProps }: AppProps) {
@ -44,7 +86,7 @@ function NextApp({ Component, pageProps }: AppProps) {
return ( return (
<NoSSRWrapper> <NoSSRWrapper>
<AppBridgeProvider appBridgeInstance={appBridgeInstance}> <AppBridgeProvider appBridgeInstance={appBridgeInstance}>
<ThemeProvider overrides={themeOverrides} ssr={false}> <ThemeProvider palettes={palettes} overrides={themeOverrides} ssr={false}>
<ThemeSynchronizer /> <ThemeSynchronizer />
<RoutePropagator /> <RoutePropagator />
<Component {...pageProps} /> <Component {...pageProps} />

View file

@ -3,23 +3,11 @@ import React, { useEffect } from "react";
import { ChannelsConfiguration } from "../modules/app-configuration/ui/channels-configuration"; import { ChannelsConfiguration } from "../modules/app-configuration/ui/channels-configuration";
import { trpcClient } from "../modules/trpc/trpc-client"; import { trpcClient } from "../modules/trpc/trpc-client";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
const ConfigurationPage: NextPage = () => { const ConfigurationPage: NextPage = () => {
const channels = trpcClient.channels.fetch.useQuery(); const channels = trpcClient.channels.fetch.useQuery();
const router = useRouter(); const router = useRouter();
const { appBridge } = useAppBridge();
const openInNewTab = (url: string) => {
appBridge?.dispatch(
actions.Redirect({
to: url,
newContext: true,
})
);
};
useEffect(() => { useEffect(() => {
if (channels.isSuccess && channels.data.length === 0) { if (channels.isSuccess && channels.data.length === 0) {
router.push("/not-ready"); router.push("/not-ready");

View file

@ -23,7 +23,7 @@ const IndexPage: NextPage = () => {
return ( return (
<div> <div>
<h1>Saleor Merchant Feed</h1> <h1>Saleor Product Feed</h1>
<p>This is Saleor App that allows product feed generation</p> <p>This is Saleor App that allows product feed generation</p>
<p>Install app in your Saleor instance and open in with Dashboard</p> <p>Install app in your Saleor instance and open in with Dashboard</p>
</div> </div>

View file

@ -2,10 +2,11 @@ body {
font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
color: #111; color: #111;
padding: 1rem 2rem;
} }
code { code {
background: #f6f8fa;
border: 1px solid #eaeaea;
border-radius: 5px; border-radius: 5px;
display: inline-block; display: inline-block;
margin-top: 10px; margin-top: 10px;
@ -13,3 +14,8 @@ code {
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace; Bitstream Vera Sans Mono, Courier New, monospace;
} }
code::before {
content: "$ ";
opacity: 0.6;
}