Compare commits

...

3 commits

Author SHA1 Message Date
Lukasz Ostrowski
7946576714 CR fixes and imprve layour 2023-06-27 12:15:33 +02:00
Lukasz Ostrowski
2b27c0c463 Rewrtirren Slack to the new macaw 2023-06-27 11:35:37 +02:00
Lukasz Ostrowski
0fb3108321 Replace material with new Macaw ui 2023-06-27 11:35:37 +02:00
12 changed files with 128 additions and 420 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-slack": minor
---
Rewritten UI to use modern macaw UI, instead legacy one with Material UI. There are slight changes in typography and spacings, but no new changes were introduced.

View file

@ -16,7 +16,9 @@
"@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.40.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "^0.7.2",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@saleor/react-hook-form-macaw": "workspace:*",
"@sentry/nextjs": "7.55.2",
"@urql/exchange-auth": "^2.1.4",
"clsx": "^1.2.1",

View file

@ -1,5 +1,5 @@
import { Typography } from "@material-ui/core";
import React from "react";
import { Text } from "@saleor/macaw-ui/next";
type WarningCause =
| "not_in_iframe"
@ -20,13 +20,13 @@ const warnings: Record<WarningCause, string> = {
export function AccessWarning({ cause = "unknown_cause" }: AccessWarningProps) {
return (
<div suppressHydrationWarning>
<Typography variant="subtitle1">
<div>
<Text as={"h2"} variant="heading">
App can&apos;t be accessed outside of the Saleor Dashboard
</Typography>
<Typography variant="subtitle2" style={{ marginTop: "2rem" }}>
</Text>
<Text variant="body" style={{ marginTop: "2rem" }}>
{warnings[cause]}
</Typography>
</Text>
</div>
);
}

View file

@ -1,21 +1,19 @@
import { makeStyles } from "@saleor/macaw-ui";
import { Box } from "@saleor/macaw-ui/next";
import { PropsWithChildren } from "react";
const useStyles = makeStyles({
root: {
display: "grid",
gridTemplateColumns: "280px auto 280px",
alignItems: "start",
gap: 32,
maxWidth: 1180,
margin: "0 auto",
},
});
type Props = PropsWithChildren<{}>;
export const AppColumnsLayout = ({ children }: Props) => {
const styles = useStyles();
return <div className={styles.root}>{children}</div>;
return (
<Box
paddingX={8}
display={"grid"}
marginTop={8}
__maxWidth={"1180px"}
__gridTemplateColumns={"380px auto"}
gap={8}
>
{children}
</Box>
);
};

View file

@ -1,18 +0,0 @@
import { CircularProgress, Typography } from "@material-ui/core";
import React from "react";
import { useStyles } from "./styles";
function LoadingPage() {
const classes = useStyles();
return (
<div className={classes.loaderContainer}>
<CircularProgress size={100} />
<Typography variant="subtitle1" className={classes.message}>
Attempting connection to Saleor Dashboard
</Typography>
</div>
);
}

View file

@ -1,14 +0,0 @@
import { makeStyles } from "@saleor/macaw-ui";
const useStyles = makeStyles((theme) => ({
loaderContainer: {
display: "flex",
flexDirection: "column",
alignItems: "center",
},
message: {
marginTop: theme.spacing(4),
},
}));
export { useStyles };

View file

@ -1,40 +1,25 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui";
import { useTheme } from "@saleor/macaw-ui/next";
import { memo, useEffect } from "react";
/**
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard,
* Macaw must be informed about this change from AppBridge.
*
* If you are not using Macaw, you can remove this.
*/
function _ThemeSynchronizer() {
const { appBridgeState, appBridge } = useAppBridge();
const { setTheme, themeType } = useTheme();
/*
* todo - replace this hook to appBridge.subscribe and react only only on initial theme event
* useEffect(() =>{
* appBridge?.subscribe('theme',console.log)
* },[appBridge])
*/
// todo move to shared
export function ThemeSynchronizer() {
const { appBridgeState } = useAppBridge();
const { setTheme } = useTheme();
useEffect(() => {
if (!setTheme || !appBridgeState?.theme) {
return;
}
if (themeType !== appBridgeState?.theme) {
setTheme(appBridgeState.theme);
/**
* Hack to fix macaw, which is going into infinite loop on light mode (probably de-sync local storage with react state)
* TODO Fix me when Macaw 2.0 is shipped
*/
window.localStorage.setItem("macaw-ui-theme", appBridgeState.theme);
if (appBridgeState.theme === "light") {
setTheme("defaultLight");
}
}, [appBridgeState?.theme, setTheme, themeType]);
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
return null;
}
export const ThemeSynchronizer = memo(_ThemeSynchronizer);

View file

@ -1,90 +1,26 @@
import "../styles/globals.css";
import { Theme } from "@material-ui/core/styles";
import "@saleor/macaw-ui/next/style";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import React from "react";
import { AppProps } from "next/app";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import {
dark,
light,
SaleorThemeColors,
ThemeProvider as MacawUIThemeProvider,
} from "@saleor/macaw-ui";
import React, { PropsWithChildren, useEffect } from "react";
import { AppLayoutProps } from "../../types";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NoSSRWrapper } from "@saleor/apps-shared";
import { ThemeSynchronizer } from "../hooks/theme-synchronizer";
import { NoSSRWrapper } from "../lib/no-ssr-wrapper";
const themeOverrides: Partial<Theme> = {
overrides: {
MuiTableCell: {
body: {
paddingBottom: 8,
paddingTop: 8,
},
root: {
height: 56,
paddingBottom: 4,
paddingTop: 4,
},
},
},
};
/**
* Ensure instance is a singleton.
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
*/
const appBridgeInstance =
typeof window !== "undefined" ? new AppBridge({ autoNotifyReady: false }) : undefined;
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
type PalettesOverride = Record<"light" | "dark", SaleorThemeColors>;
/**
* 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)",
},
},
};
/**
* That's a hack required by Macaw-UI incompatibility with React@18
*/
const ThemeProvider = MacawUIThemeProvider as React.FC<
PropsWithChildren<{ overrides?: Partial<Theme>; ssr: boolean; palettes: PalettesOverride }>
>;
function SaleorApp({ Component, pageProps }: AppLayoutProps) {
function SaleorApp({ Component, pageProps }: AppProps) {
// @ts-ignore todo refactor
const getLayout = Component.getLayout ?? ((page) => page);
useEffect(() => {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles) {
jssStyles?.parentElement?.removeChild(jssStyles);
}
}, []);
return (
<NoSSRWrapper>
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
<ThemeProvider palettes={palettes} overrides={themeOverrides} ssr>
<ThemeProvider>
<ThemeSynchronizer />
<RoutePropagator />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</AppBridgeProvider>

View file

@ -1,43 +0,0 @@
/**
* NOTE: This requires `@sentry/nextjs` version 7.3.0 or higher.
*
* NOTE: If using this with `next` version 12.2.0 or lower, uncomment the
* penultimate line in `CustomErrorComponent`.
*
* This page is loaded by Nextjs:
* - on the server, when data-fetching methods throw or reject
* - on the client, when `getInitialProps` throws or rejects
* - on the client, when a React lifecycle method throws or rejects, and it's
* caught by the built-in Nextjs error boundary
*
* See:
* - https://nextjs.org/docs/basic-features/data-fetching/overview
* - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props
* - https://reactjs.org/docs/error-boundaries.html
*/
import * as Sentry from "@sentry/nextjs";
import NextErrorComponent from "next/error";
const CustomErrorComponent = (props) => {
/*
* If you're using a Nextjs version prior to 12.2.1, uncomment this to
* compensate for https://github.com/vercel/next.js/issues/8592
* Sentry.captureUnderscoreErrorException(props);
*/
return <NextErrorComponent statusCode={props.statusCode} />;
};
CustomErrorComponent.getInitialProps = async (contextData) => {
/*
* In case this is running in a serverless function, await this in order to give Sentry
* time to send the error before the lambda exits
*/
await Sentry.captureUnderscoreErrorException(contextData);
// This will contain the status code of the response
return NextErrorComponent.getInitialProps(contextData);
};
export default CustomErrorComponent;

View file

@ -1,48 +1,26 @@
import {
Card,
CardContent,
CardHeader,
Link,
List,
ListItem,
TextField,
Typography,
} from "@material-ui/core";
import Skeleton from "@material-ui/lab/Skeleton";
import { useAppBridge, withAuthorization } from "@saleor/app-sdk/app-bridge";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { ConfirmButton, ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui";
import { ChangeEvent, ReactElement, SyntheticEvent, useEffect, useState } from "react";
import { AccessWarning } from "../components/AccessWarning/AccessWarning";
import { ConfigurationError } from "../components/ConfigurationError/ConfigurationError";
import { useAppApi } from "../hooks/useAppApi";
import { AppColumnsLayout } from "../components/AppColumnsLayout/AppColumnsLayout";
import { useDashboardNotification } from "@saleor/apps-shared";
import { Input, Text, Box, Button } from "@saleor/macaw-ui/next";
import { TextLink } from "@saleor/apps-ui";
import { AccessWarning } from "../components/AccessWarning/AccessWarning";
interface ConfigurationField {
key: string;
value: string;
}
const useStyles = makeStyles((theme) => ({
confirmButton: {
marginLeft: "auto",
},
fieldContainer: {
marginBottom: theme.spacing(2),
},
additionalInfo: {
marginBottom: theme.spacing(3),
},
}));
function Configuration() {
const classes = useStyles();
const { appBridgeState } = useAppBridge();
const { notifyError, notifySuccess } = useDashboardNotification();
const [configuration, setConfiguration] = useState<ConfigurationField[]>();
const [transitionState, setTransitionState] = useState<ConfirmButtonTransitionState>("default");
const { data: configurationData, error } = useAppApi<{ data: ConfigurationField[] }>({
url: "/api/configuration",
@ -56,7 +34,6 @@ function Configuration() {
const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault();
setTransitionState("loading");
fetch("/api/configuration", {
method: "POST",
@ -68,11 +45,9 @@ function Configuration() {
body: JSON.stringify({ data: configuration }),
})
.then(async (response) => {
setTransitionState(response.status === 200 ? "success" : "error");
notifySuccess("Success", "Configuration updated successfully");
})
.catch(async () => {
setTransitionState("error");
await notifyError("Configuration update failed");
});
};
@ -91,33 +66,29 @@ function Configuration() {
}
if (configuration === undefined) {
return <Skeleton />;
return <Text>Loading</Text>;
}
return (
<form onSubmit={handleSubmit}>
{configuration!.map(({ key, value }) => (
<div key={key} className={classes.fieldContainer}>
<TextField label={key} name={key} fullWidth onChange={onChange} value={value} />
<div key={key}>
<Input
label={key}
name={key}
onChange={onChange}
value={value}
helperText={
"This webhook will be called when new order is created and `order_created` event is triggered."
}
/>
</div>
))}
<p className={classes.additionalInfo}>
This webhook will be called when new order is created and `order_created` event is
triggered.
</p>
<div>
<ConfirmButton
type="submit"
variant="primary"
fullWidth
transitionState={transitionState}
labels={{
confirm: "Save",
error: "Error",
}}
className={classes.confirmButton}
/>
</div>
<Box marginTop={4}>
<Button type="submit" variant="primary">
Save
</Button>
</Box>
</form>
);
}
@ -147,50 +118,29 @@ function Instructions() {
return (
<>
<Typography>How to configure</Typography>
<List>
<ListItem>
<Link
onClick={(e) => {
e.preventDefault();
openExternalUrl(slackUrl.href);
}}
href={slackUrl.href}
>
Install Slack application
</Link>
</ListItem>
<ListItem>
Copy incoming Webhook URL from Slack app configuration and paste it below into
`WEBHOOK_URL` field
</ListItem>
<ListItem>Save configuration</ListItem>
</List>
<Typography>Useful links</Typography>
<List>
<ListItem>
<Link
onClick={(e) => {
e.preventDefault();
openExternalUrl("https://github.com/saleor/saleor-app-slack");
}}
href="https://github.com/saleor/saleor-app-slack"
>
Visit repository & readme
</Link>
</ListItem>
<ListItem>
<Link
onClick={(e) => {
e.preventDefault();
openExternalUrl("https://api.slack.com/messaging/webhooks");
}}
href="https://api.slack.com/messaging/webhooks"
>
<Text variant={"heading"}>How to configure</Text>
<Box display={"flex"} gap={2} as={"ul"} flexDirection={"column"}>
<li>
<TextLink href={slackUrl.href}>1. Install Slack application</TextLink>
</li>
<li>
<Text>
2. Copy incoming Webhook URL from Slack app configuration and paste it below into{" "}
<Text variant={"bodyStrong"}>WEBHOOK_URL</Text> field
</Text>
</li>
<li>
<Text>3. Save configuration</Text>
</li>
</Box>
<Text variant={"heading"}>Useful links</Text>
<ul>
<li>
<TextLink newTab href={"https://api.slack.com/messaging/webhooks"}>
Read about Slack apps that use incoming webhooks
</Link>
</ListItem>
</List>
</TextLink>
</li>
</ul>
</>
);
}
@ -204,17 +154,21 @@ const ConfigurationWithAuth = withAuthorization({
ConfigurationWithAuth.getLayout = (page: ReactElement) => (
<AppColumnsLayout>
<div />
<Card>
<CardHeader title="Configuration" />
<CardContent>{page}</CardContent>
</Card>
<Card style={{ marginBottom: 40 }}>
<CardHeader title="Instructions" />
<CardContent>
<Instructions />
</CardContent>
</Card>
<Box marginBottom={4}>
<Instructions />
</Box>
<Box
borderColor={"neutralHighlight"}
borderStyle={"solid"}
borderWidth={1}
padding={4}
borderRadius={4}
>
<Text as={"h2"} marginBottom={4} variant={"heading"}>
Configuration
</Text>
<Box>{page}</Box>
</Box>
</AppColumnsLayout>
);

View file

@ -1,85 +1,15 @@
import { NextPage } from "next";
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useRouter } from "next/router";
import React, { FormEventHandler, useEffect } from "react";
import { useEffect } from "react";
import { useIsMounted } from "usehooks-ts";
import Image from "next/image";
import SaleorLogoImage from "../assets/saleor-logo.svg";
import SaleorLogoImageDark from "../assets/saleor-logo-dark.svg";
import { InputAdornment, LinearProgress, TextField, Typography } from "@material-ui/core";
import { Button, makeStyles, useTheme } from "@saleor/macaw-ui";
import { useRouter } from "next/router";
import { isInIframe } from "@saleor/apps-shared";
import { Box, Text } from "@saleor/macaw-ui/next";
const useStyles = makeStyles({
root: {
maxWidth: 960,
margin: "50px auto",
},
headline: {
marginTop: 70,
lineHeight: 1.5,
},
grid: {
display: "grid",
gridTemplateColumns: "50% 50%",
gap: 116,
marginTop: 48,
},
form: {
marginTop: 24,
},
submitButton: {
marginTop: 12,
},
buttons: {
marginTop: 24,
"& > *": {
marginBottom: 12,
},
},
});
const Input = () => {
return (
<TextField
label="Your Saleor URL"
fullWidth
name="saleor-url"
InputProps={{
endAdornment: <InputAdornment position="end">.saleor.cloud</InputAdornment>,
}}
/>
);
};
/**
* Common landing page in case of app being open outside the Dashboard.
* Allows quick installation with Saleor env input
*
* Can be safely removed
*/
const IndexPage: NextPage = () => {
const styles = useStyles();
const { appBridgeState } = useAppBridge();
const isMounted = useIsMounted();
const { replace } = useRouter();
const { themeType } = useTheme();
const onFormSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
const appOrigin = window.location.origin;
const appManifestUrl = new URL("api/manifest", appOrigin);
const saleorUrlSlug = new FormData(e.currentTarget).get("saleor-url") as string;
const saleorUrl = new URL("https://" + saleorUrlSlug.replace("https://", "") + ".saleor.cloud");
const installationUrl = new URL(
`dashboard/apps/install?manifestUrl=${appManifestUrl}`,
saleorUrl
).href;
window.location.href = installationUrl;
};
useEffect(() => {
if (isMounted() && appBridgeState?.ready) {
@ -87,54 +17,21 @@ const IndexPage: NextPage = () => {
}
}, [isMounted, appBridgeState?.ready]);
/**
* TODO Check also some timeout and show error if appBridge never handshakes
*/
if (isInIframe()) {
return <LinearProgress />;
return null;
}
return (
<div className={styles.root}>
<Image
alt="Saleor logo"
width={200}
src={themeType === "light" ? SaleorLogoImage : SaleorLogoImageDark}
/>
<Typography className={styles.headline} variant="h1">
The Slack App has to be <br />
launched in the Saleor Dashboard
</Typography>
<div className={styles.grid}>
<div>
<Typography variant="h3">
Provide you Saleor URL
<br /> to quickly install the app
</Typography>
<form onSubmit={onFormSubmit} className={styles.form}>
<Input />
<Button type="submit" className={styles.submitButton} fullWidth variant="primary">
{" "}
Submit and start installation
</Button>
</form>
</div>
<div>
<Typography variant="h3">
Or check the instructions
<br /> and see how to install the app
</Typography>
<div className={styles.buttons}>
<Button variant="secondary" fullWidth>
Open repository
</Button>
<Button variant="secondary" fullWidth>
See Saleor Docs
</Button>
</div>
</div>
</div>
</div>
<Box>
<Text as={"h1"} variant={"hero"}>
Saleor Slack App
</Text>
<Text as={"p"}>This is Saleor App that allows invoices generation</Text>
<Text as={"p"}>
Install app in your Saleor instance and open in with Dashboard{" "}
<a href={"https://github.com/saleor/apps"}>or check it on GitHub</a>
</Text>
</Box>
);
};

View file

@ -1273,9 +1273,15 @@ importers:
'@saleor/apps-shared':
specifier: workspace:*
version: link:../../packages/shared
'@saleor/apps-ui':
specifier: workspace:*
version: link:../../packages/ui
'@saleor/macaw-ui':
specifier: ^0.7.2
version: 0.7.3(@material-ui/core@4.12.4)(@material-ui/icons@4.11.3)(@material-ui/lab@4.0.0-alpha.61)(@types/react@18.2.5)(react-dom@18.2.0)(react-helmet@6.1.0)(react@18.2.0)
specifier: 0.8.0-pre.95
version: 0.8.0-pre.95(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@saleor/react-hook-form-macaw':
specifier: workspace:*
version: link:../../packages/react-hook-form-macaw
'@sentry/nextjs':
specifier: 7.55.2
version: 7.55.2(next@13.3.0)(react@18.2.0)