Extract shared packages (#948)

* [skip ci] tRPC shared package

* [skip ci] tRPC shared package - fix

* [skip ci] shared package - app sections

* [skip ci] segment - implement shared components

* [skip ci] extract theme synchronizer

* extract components and implement them in apps

* cms - extract shared packages

* Fix imports

* remove urql from peer deps
This commit is contained in:
Lukasz Ostrowski 2023-09-04 11:30:57 +02:00 committed by GitHub
parent ceddcf96eb
commit e8660e8bb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 679 additions and 670 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-cms-v2": patch
---
Extracted UI components and use shared package

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": patch
---
Implemented shared getApBaseUrl

View file

@ -0,0 +1,5 @@
---
"@saleor/apps-shared": minor
---
Added GraphQL Provider component that build client side graphql context

View file

@ -0,0 +1,5 @@
---
"saleor-app-segment": patch
---
Implemented ButtonsBox and SkeletonLayout from shared package. This should not have visual effect other than better looking Skeleton animation

View file

@ -0,0 +1,6 @@
---
"saleor-app-segment": patch
"saleor-app-search": patch
---
Replaced GraphQL provider with shared package

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": patch
---
Replaced AppSections implementation with the shared package

View file

@ -0,0 +1,5 @@
---
"@saleor/apps-ui": minor
---
Added two new components: ButtonsBox, which is a simple grid wrapper for horizontal buttons and SkeletonSection which composes Macaw Skeletons and make them looking more like a layout

View file

@ -0,0 +1,5 @@
---
"@saleor/apps-shared": patch
---
Removed ThemeProvider that was legacy for the older Macaw/Material UI

View file

@ -0,0 +1,6 @@
---
"saleor-app-segment": patch
"saleor-app-search": patch
---
Implemented ThemeSynchronizer from shared package

View file

@ -0,0 +1,5 @@
---
"@saleor/trpc": major
---
Introduced shared tRPC package. It contains reusable utils that are shared among apps

View file

@ -0,0 +1,5 @@
---
"@saleor/apps-ui": minor
---
Added Layout.AppSection and Layout.AppSectionCard components to build standard app layouts

View file

@ -0,0 +1,5 @@
---
"@saleor/apps-shared": minor
---
Added getAppBaseUrl function that infers apps own URL

View file

@ -0,0 +1,5 @@
---
"saleor-app-segment": patch
---
Extracted some tRPC utilities to shared package

View file

@ -0,0 +1,5 @@
---
"@saleor/apps-shared": minor
---
Added ThemeSynchronizer component that automatically syncs AppBridge and Macaw theme

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": patch
---
Fixed broken configuration form when legacy metadata was fetched

View file

@ -33,6 +33,13 @@ updates:
- package-ecosystem: "npm"
directory: "/packages/react-hook-form-macaw"
open-pull-requests-limit: 1
schedule:
interval: "weekly"
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: "/packages/trpc"
open-pull-requests-limit: 1
schedule:
interval: "weekly"
commit-message:

View file

@ -6,26 +6,20 @@ import { Select } from "@saleor/react-hook-form-macaw";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { ButtonsBox } from "../ui/buttons-box";
import { ProvidersResolver } from "../providers/providers-resolver";
import { Skeleton } from "../ui/skeleton";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
const FormSchema = z.object({
connectionId: z.string().min(7),
});
const EmptyState = () => (
<Box
display="flex"
paddingY={4}
flexDirection={"column"}
gap={4}
alignItems={"center"}
justifyContent={"center"}
>
<Text variant="heading">No connections configured</Text>
<Text>Create a channel connection above to enable bulk synchronization.</Text>
</Box>
<Layout.AppSectionCard>
<Box display="flex" flexDirection={"column"} gap={4} justifyContent={"center"}>
<Text variant="heading">Bulk products synchronization</Text>
<Text>Create a channel connection above to enable bulk synchronization.</Text>
</Box>
</Layout.AppSectionCard>
);
export const BulkSyncSection = () => {
@ -42,7 +36,7 @@ export const BulkSyncSection = () => {
});
if (!connections || !providers) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
if (connections.length === 0) {
@ -50,7 +44,7 @@ export const BulkSyncSection = () => {
}
return (
<Box>
<Layout.AppSectionCard>
<Text as="h2" marginBottom={6} variant="heading">
Bulk products synchronization
</Text>
@ -88,6 +82,6 @@ export const BulkSyncSection = () => {
<Button type="submit">Start sync</Button>
</ButtonsBox>
</Box>
</Box>
</Layout.AppSectionCard>
);
};

View file

@ -1,10 +1,8 @@
import { Breadcrumbs } from "@saleor/apps-ui";
import { Breadcrumbs, ButtonsBox, Layout } from "@saleor/apps-ui";
import { ArrowRightIcon, Box, Button, Text } from "@saleor/macaw-ui/next";
import { useEffect, useRef, useState } from "react";
import { ChannelProviderConnectionConfig, ProvidersConfig } from "../configuration";
import { AppHeader } from "../ui/app-header";
import { AppSection } from "../ui/app-section";
import { ButtonsBox } from "../ui/buttons-box";
import { useBulkSyncProductsState } from "./use-bulk-sync-products-state";
import { useFetchAllProducts } from "./use-fetch-all-products";
@ -14,16 +12,19 @@ import { useDashboardNotification } from "@saleor/apps-shared";
const FetchProductsStep = (props: { onButtonClick(): void }) => {
return (
<Box>
<Layout.AppSectionCard
footer={
<ButtonsBox>
<Button onClick={props.onButtonClick}>Prefetch products</Button>
</ButtonsBox>
}
>
<Text variant="heading" as="h2" marginBottom={4}>
Saleor products fetch
</Text>
<Text as="p">Click the button to start fetching products from Saleor API</Text>
<Text as="p">After products are fetched, you will be able to upload them to the CMS</Text>
<ButtonsBox>
<Button onClick={props.onButtonClick}>Prefetch products</Button>
</ButtonsBox>
</Box>
</Layout.AppSectionCard>
);
};
@ -66,7 +67,7 @@ export const BulkSyncView = ({
const { products, finished: saleorProductsFetchFinished } = useFetchAllProducts(
state === "fetching",
connection.channelSlug
connection.channelSlug,
);
const { productsStatusList, setInitialProducts, setItemStatus, finished } =
@ -121,9 +122,14 @@ export const BulkSyncView = ({
]}
/>
<AppSection
<Layout.AppSection
marginBottom={8}
mainContent={(() => {
heading="1. Fetch products"
sideContent={
<Text>First pre-fetch all Product Variants from Saleor. Do not close the app.</Text>
}
>
{(() => {
switch (state) {
case "initial": {
return (
@ -149,33 +155,29 @@ export const BulkSyncView = ({
}
}
})()}
heading="1. Fetch products"
sideContent={
<Text>First pre-fetch all Product Variants from Saleor. Do not close the app.</Text>
}
/>
</Layout.AppSection>
{(state === "fetched" || state === "uploading") && productsStatusList && (
<AppSection
<Layout.AppSection
marginTop={14}
heading="2. Upload to the CMS"
sideContent={<Text>Send listed variants to the CMS</Text>}
mainContent={
<Box>
<Text as="h2" marginBottom={4} variant="heading">
Upload products
</Text>
{state === "fetched" && (
<Box marginBottom={4}>
<Text as="p" marginBottom={2}>
Verify products below and click the button to start uploading.
</Text>
<Button onClick={() => setState("uploading")}>Start uploading</Button>
</Box>
)}
<VariantsSyncStatusList marginTop={8} variants={productsStatusList} />
</Box>
}
/>
>
<Layout.AppSectionCard>
<Text as="h2" marginBottom={4} variant="heading">
Upload products
</Text>
{state === "fetched" && (
<Box marginBottom={4}>
<Text as="p" marginBottom={2}>
Verify products below and click the button to start uploading.
</Text>
<Button onClick={() => setState("uploading")}>Start uploading</Button>
</Box>
)}
<VariantsSyncStatusList marginTop={8} variants={productsStatusList} />
</Layout.AppSectionCard>
</Layout.AppSection>
)}
</Box>
);

View file

@ -1,5 +1,4 @@
import { Button, Text } from "@saleor/macaw-ui/next";
import { ButtonsBox } from "../ui/buttons-box";
import { Modal } from "../ui/modal";
import {
AddConnectionForm,
@ -7,7 +6,7 @@ import {
AddConnectionFormSchema,
} from "./add-connection-form";
import { trpcClient } from "../trpc/trpc-client";
import { Skeleton } from "../ui/skeleton";
import { ButtonsBox, SkeletonLayout } from "@saleor/apps-ui";
const defaultValues: AddConnectionFormSchema = { channelSlug: "", providerId: "" };
@ -15,7 +14,7 @@ export const AddConnectionModal = (props: { onSuccess(): void; onClose(): void }
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
if (!providers) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
const { mutateAsync: addProviderMutate, isLoading } =

View file

@ -1,35 +1,31 @@
import { useDashboardNotification } from "@saleor/apps-shared";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { useState } from "react";
import { trpcClient } from "../trpc/trpc-client";
import { ButtonsBox } from "../ui/buttons-box";
import { AddConnectionFormSchema } from "./add-connection-form";
import { AddConnectionModal } from "./add-connection-modal";
import { ChanelProviderConnectionsSectionHeader } from "./channel-provider-connections-section-header";
import { ConnectionsList } from "./connections-list";
import { Skeleton } from "../ui/skeleton";
const NoConnections = (props: { onCreate(): void; enabled: boolean }) => (
<Box>
<ChanelProviderConnectionsSectionHeader />
<Text marginBottom={4} as="p">
<Text as="p">
No channels connected yet.{" "}
{!props.enabled &&
"Ensure you have created a provider configuration that can be connected first."}
</Text>
{props.enabled && (
<ButtonsBox>
<Button onClick={props.onCreate}>Create first connection</Button>
</ButtonsBox>
)}
</Box>
);
export const ChannelProviderConnectionList = () => {
const [dialogOpen, setDialogOpen] = useState(false);
const { data: connectionsData, refetch: refetchConnections } =
trpcClient.channelsProvidersConnection.fetchConnections.useQuery();
const {
data: connectionsData,
refetch: refetchConnections,
isLoading,
} = trpcClient.channelsProvidersConnection.fetchConnections.useQuery();
const { mutate: removeConnection } =
trpcClient.channelsProvidersConnection.removeConnection.useMutation({
@ -45,19 +41,33 @@ export const ChannelProviderConnectionList = () => {
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
if (!providers) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
const handleDelete = (connectionId: string) => {
removeConnection({ id: connectionId });
};
if (!connectionsData) {
return <Text>Loading</Text>;
if (isLoading || !connectionsData) {
return <SkeletonLayout.Section />;
}
return (
<Box>
<Layout.AppSectionCard
footer={
providers.length > 0 && (
<ButtonsBox>
<Button
onClick={() => {
setDialogOpen(true);
}}
>
Add connection
</Button>
</ButtonsBox>
)
}
>
{dialogOpen && (
<AddConnectionModal
onClose={() => {
@ -79,17 +89,6 @@ export const ChannelProviderConnectionList = () => {
/>
)}
{connectionsData.length > 0 && <ConnectionsList onRemove={handleDelete} />}
{connectionsData.length > 0 && (
<ButtonsBox marginTop={6}>
<Button
onClick={() => {
setDialogOpen(true);
}}
>
Add connection
</Button>
</ButtonsBox>
)}
</Box>
</Layout.AppSectionCard>
);
};

View file

@ -4,7 +4,7 @@ import React from "react";
import { trpcClient } from "../trpc/trpc-client";
import { ChanelProviderConnectionsSectionHeader } from "./channel-provider-connections-section-header";
import { ProvidersResolver } from "../providers/providers-resolver";
import { Skeleton } from "../ui/skeleton";
import { SkeletonLayout } from "@saleor/apps-ui";
export const ConnectionsList = (props: { onRemove(connectionId: string): void }) => {
const { data } = trpcClient.channelsProvidersConnection.fetchConnections.useQuery();
@ -12,7 +12,7 @@ export const ConnectionsList = (props: { onRemove(connectionId: string): void })
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
if (!data || !providers) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
return (

View file

@ -5,8 +5,8 @@ import { ProvidersConfig } from "../configuration";
import { ProvidersResolver } from "../providers/providers-resolver";
import { trpcClient } from "../trpc/trpc-client";
import { ButtonsBox } from "../ui/buttons-box";
import { Skeleton } from "../ui/skeleton";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
const ProvidersTable = (props: { providers: ProvidersConfig.AnyFullShape[] }) => {
const { push } = useRouter();
@ -47,30 +47,45 @@ export const ProvidersList = () => {
const { push } = useRouter();
if (!data) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
if (data.length === 0) {
return (
<Box>
<Layout.AppSectionCard
footer={
<ButtonsBox>
<Button
onClick={() => {
push("/add-provider");
}}
>
Add first CMS configuration
</Button>
</ButtonsBox>
}
>
<Text as="p" marginBottom={4}>
No configurations yet
</Text>
</Layout.AppSectionCard>
);
}
return (
<Layout.AppSectionCard
footer={
<ButtonsBox>
<Button
onClick={() => {
push("/add-provider");
}}
>
Add first CMS configuration
Add CMS configuration
</Button>
</ButtonsBox>
</Box>
);
}
return (
<Box>
}
>
{data.length && (
<Box>
<Text variant="heading" as="h2" marginBottom={4}>
@ -79,15 +94,6 @@ export const ProvidersList = () => {
<ProvidersTable providers={data} />
</Box>
)}
<ButtonsBox marginTop={8}>
<Button
onClick={() => {
push("/add-provider");
}}
>
Add CMS configuration
</Button>
</ButtonsBox>
</Box>
</Layout.AppSectionCard>
);
};

View file

@ -7,9 +7,7 @@ import { useForm } from "react-hook-form";
import { BuilderIoProviderConfig, SaleorProviderFieldsMappingKeys } from "../../configuration";
import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields";
import { trpcClient } from "../../trpc/trpc-client";
import { ButtonsBox } from "../../ui/buttons-box";
import { TextLink } from "@saleor/apps-ui";
import { Skeleton } from "@/modules/ui/skeleton";
import { ButtonsBox, SkeletonLayout, TextLink } from "@saleor/apps-ui";
type FormShape = Omit<BuilderIoProviderConfig.InputShape, "type">;
const FormSchema = BuilderIoProviderConfig.Schema.Input.omit({ type: true });
@ -207,7 +205,7 @@ const EditFormVariant = (props: { configId: string }) => {
});
if (!data) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
if (data.type !== "builder.io") {

View file

@ -9,8 +9,7 @@ import { useDashboardNotification } from "@saleor/apps-shared";
import { ContentfulProviderConfig } from "../../configuration/schemas/contentful-provider.schema";
import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields";
import { zodResolver } from "@hookform/resolvers/zod";
import { ButtonsBox } from "../../ui/buttons-box";
import { TextLink } from "@saleor/apps-ui";
import { ButtonsBox, TextLink } from "@saleor/apps-ui";
import { SaleorProviderFieldsMappingKeys } from "@/modules/configuration";
type FormSchema = Omit<ContentfulProviderConfig.InputShape, "type">;

View file

@ -8,8 +8,8 @@ import React, { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields";
import { trpcClient } from "../../trpc/trpc-client";
import { ButtonsBox } from "../../ui/buttons-box";
import { DatocmsProviderConfig } from "@/modules/configuration/schemas/datocms-provider.schema";
import { ButtonsBox } from "@saleor/apps-ui";
type FormShape = Omit<DatocmsProviderConfig.InputShape, "type">;

View file

@ -9,8 +9,7 @@ import React from "react";
import { useForm } from "react-hook-form";
import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields";
import { trpcClient } from "../../trpc/trpc-client";
import { ButtonsBox } from "../../ui/buttons-box";
import { TextLink } from "@saleor/apps-ui";
import { ButtonsBox, TextLink } from "@saleor/apps-ui";
type FormShape = Omit<PayloadCmsProviderConfig.InputShape, "type">;

View file

@ -7,8 +7,7 @@ import { useForm } from "react-hook-form";
import { SaleorProviderFieldsMappingKeys, StrapiProviderConfig } from "../../configuration";
import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields";
import { trpcClient } from "../../trpc/trpc-client";
import { ButtonsBox } from "../../ui/buttons-box";
import { Skeleton } from "@/modules/ui/skeleton";
import { ButtonsBox, SkeletonLayout } from "@saleor/apps-ui";
type FormShape = Omit<StrapiProviderConfig.InputShape, "type">;
@ -189,7 +188,7 @@ const EditFormVariant = (props: { configId: string }) => {
});
if (!data) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
if (data.type !== "strapi") {

View file

@ -1,10 +1,10 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
import { inferAsyncReturnType } from "@trpc/server";
import { getBaseUrl } from "@/modules/shared/get-base-url";
import { getAppBaseUrl } from "@saleor/apps-shared";
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
const baseUrl = getBaseUrl(req.headers);
const baseUrl = getAppBaseUrl(req.headers);
return {
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,

View file

@ -1,43 +0,0 @@
import { Box, PropsWithBox, Text } from "@saleor/macaw-ui/next";
import { ReactNode } from "react";
// todo move to shared
export const AppSection = ({
heading,
sideContent,
mainContent,
includePadding = true,
...props
}: PropsWithBox<{
heading: string;
sideContent?: ReactNode;
mainContent: ReactNode;
includePadding?: boolean;
}>) => {
return (
<Box
as="section"
__gridTemplateColumns={"400px auto"}
display={"grid"}
gap={10}
__maxWidth={"1200px"}
{...props}
>
<Box>
<Text as="h2" variant={"heading"} size={"large"} marginBottom={1.5}>
{heading}
</Text>
{sideContent}
</Box>
<Box
borderStyle={"solid"}
borderColor={"neutralPlain"}
borderWidth={1}
padding={includePadding ? 5 : 0}
borderRadius={4}
>
{mainContent}
</Box>
</Box>
);
};

View file

@ -1,29 +0,0 @@
import { Box, BoxProps } from "@saleor/macaw-ui/next";
// TODO: Make it more generic, move to shared or contribute to macaw
const Section = (props: BoxProps) => {
return (
<Box display="grid" gap={2} {...props}>
<Box
__height="10px"
backgroundColor="surfaceNeutralHighlight"
borderRadius={2}
__width="50%"
/>
<Box
__height="10px"
backgroundColor="surfaceNeutralHighlight"
borderRadius={2}
__width="70%"
/>
<Box
__height="10px"
backgroundColor="surfaceNeutralHighlight"
borderRadius={2}
__width="60%"
/>
</Box>
);
};
export const Skeleton = { Section };

View file

@ -1,7 +1,6 @@
import { CMSProviders } from "@/modules/providers/providers-registry";
import { AppHeader } from "@/modules/ui/app-header";
import { AppSection } from "@/modules/ui/app-section";
import { Breadcrumbs } from "@saleor/apps-ui";
import { Breadcrumbs, Layout } from "@saleor/apps-ui";
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { NextPage } from "next";
import { useRouter } from "next/router";
@ -16,14 +15,15 @@ const AddProviderPage: NextPage = () => {
text="Connect CMS platforms to the App."
breadcrumbs={[<Breadcrumbs.Item key="provider">Add Provider</Breadcrumbs.Item>]}
/>
<AppSection
<Layout.AppSection
heading="Select CMS provider"
sideContent={
<Box>
<Text>App allows to connect one or more CMS platforms. You can add more later.</Text>
</Box>
}
mainContent={
>
<Layout.AppSectionCard>
<Box
display="grid"
__gridTemplateColumns="auto auto auto"
@ -54,8 +54,8 @@ const AddProviderPage: NextPage = () => {
</React.Fragment>
))}
</Box>
}
/>
</Layout.AppSectionCard>
</Layout.AppSection>
</Box>
);
};

View file

@ -2,8 +2,7 @@ import { CMSType } from "@/modules/providers/providers-registry";
import { ProvidersResolver } from "@/modules/providers/providers-resolver";
import { AppHeader } from "@/modules/ui/app-header";
import { AppSection } from "@/modules/ui/app-section";
import { Breadcrumbs } from "@saleor/apps-ui";
import { Breadcrumbs, Layout } from "@saleor/apps-ui";
import { Box, Text } from "@saleor/macaw-ui/next";
import { NextPage } from "next";
import { useRouter } from "next/router";
@ -32,7 +31,7 @@ const AddProviderPage: NextPage = () => {
]}
/>
<AppSection
<Layout.AppSection
heading={`Set up ${provider.displayName}`}
sideContent={
<Box>
@ -40,8 +39,11 @@ const AddProviderPage: NextPage = () => {
{provider.formSideInfo && <Box marginTop={6}>{provider.formSideInfo}</Box>}
</Box>
}
mainContent={<FormComponent />}
/>
>
<Layout.AppSectionCard>
<FormComponent />
</Layout.AppSectionCard>
</Layout.AppSection>
</Box>
);
};

View file

@ -1,10 +1,9 @@
import { BulkSyncView } from "@/modules/bulk-sync/bulk-sync-view";
import { trpcClient } from "@/modules/trpc/trpc-client";
import { SkeletonLayout } from "@saleor/apps-ui";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { z } from "zod";
import { Text } from "@saleor/macaw-ui/next";
import { Skeleton } from "@/modules/ui/skeleton";
const BulkSyncPage: NextPage = () => {
const { query } = useRouter();
@ -23,7 +22,7 @@ const BulkSyncPage: NextPage = () => {
},
{
enabled: !!parsedID,
}
},
);
const {
@ -36,7 +35,7 @@ const BulkSyncPage: NextPage = () => {
},
{
enabled: !!connection,
}
},
);
if ((providerFetched && !provider) || (connectionFetched && !connection)) {
@ -45,7 +44,7 @@ const BulkSyncPage: NextPage = () => {
}
if (connectionLoading || providerLoading) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
if (!(provider && connection)) {

View file

@ -2,7 +2,7 @@ import { BulkSyncSection } from "@/modules/bulk-sync/bulk-sync-section";
import { ChannelProviderConnectionList } from "@/modules/channel-provider-connection/channels-provider-connection-list";
import { ProvidersList } from "@/modules/providers-listing/providers-list";
import { AppHeader } from "@/modules/ui/app-header";
import { AppSection } from "@/modules/ui/app-section";
import { Layout } from "@saleor/apps-ui";
import { Box, Text } from "@saleor/macaw-ui/next";
import { NextPage } from "next";
@ -10,7 +10,7 @@ const ConfigurationPage: NextPage = () => {
return (
<Box>
<AppHeader />
<AppSection
<Layout.AppSection
marginBottom={14}
heading="Providers configuration"
sideContent={
@ -18,9 +18,10 @@ const ConfigurationPage: NextPage = () => {
<Text>Configure one or more CMS providers to synchronize Saleor products.</Text>
</Box>
}
mainContent={<ProvidersList />}
/>
<AppSection
>
<ProvidersList />
</Layout.AppSection>
<Layout.AppSection
marginBottom={14}
heading="Automatic synchronization"
sideContent={
@ -31,9 +32,10 @@ const ConfigurationPage: NextPage = () => {
</Text>
</Box>
}
mainContent={<ChannelProviderConnectionList />}
/>
<AppSection
>
<ChannelProviderConnectionList />
</Layout.AppSection>
<Layout.AppSection
heading="Initial sync"
sideContent={
<Box>
@ -44,8 +46,9 @@ const ConfigurationPage: NextPage = () => {
<Text as="p">Its recommended to run this flow initially, once app is configured.</Text>
</Box>
}
mainContent={<BulkSyncSection />}
/>
>
<BulkSyncSection />
</Layout.AppSection>
</Box>
);
};

View file

@ -2,9 +2,7 @@ import { ProvidersResolver } from "@/modules/providers/providers-resolver";
import { trpcClient } from "@/modules/trpc/trpc-client";
import { AppHeader } from "@/modules/ui/app-header";
import { AppSection } from "@/modules/ui/app-section";
import { Skeleton } from "@/modules/ui/skeleton";
import { Breadcrumbs } from "@saleor/apps-ui";
import { Breadcrumbs, Layout, SkeletonLayout } from "@saleor/apps-ui";
import { Box, Text } from "@saleor/macaw-ui/next";
import { NextPage } from "next";
import { useRouter } from "next/router";
@ -20,7 +18,7 @@ const EditProviderPage: NextPage = () => {
},
{
enabled: !!configId,
}
},
);
const provider = useMemo(() => {
@ -28,7 +26,7 @@ const EditProviderPage: NextPage = () => {
}, [data]);
if (isLoading) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
if (isFetched && !data) {
@ -38,7 +36,7 @@ const EditProviderPage: NextPage = () => {
}
if (!provider) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
const EditForm = ProvidersResolver.getEditProviderFormComponent(provider.type);
@ -53,13 +51,16 @@ const EditProviderPage: NextPage = () => {
<Breadcrumbs.Item key="configname">{data?.configName}</Breadcrumbs.Item>,
]}
/>
<AppSection
<Layout.AppSection
heading="Edit CMS configuration"
mainContent={<EditForm configId={configId} />}
sideContent={
<Box>{provider.formSideInfo && <Box marginTop={6}>{provider.formSideInfo}</Box>}</Box>
}
/>
>
<Layout.AppSectionCard>
<EditForm configId={configId} />
</Layout.AppSectionCard>
</Layout.AppSection>
</Box>
);
};

View file

@ -13,6 +13,7 @@ import {
} from "../modules/configuration/configuration";
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
import { trpcClient } from "../modules/trpc/trpc-client";
import { Layout } from "@saleor/apps-ui";
export const AlgoliaConfigurationForm = () => {
const { notifyError, notifySuccess } = useDashboardNotification();
@ -76,56 +77,53 @@ export const AlgoliaConfigurationForm = () => {
const isFormDisabled = isMutationLoading || isQueryLoading;
return (
<Box>
<form onSubmit={onFormSubmit}>
<Box padding={5}>
<Box marginBottom={5}>
<Input
control={control}
name="appId"
disabled={isFormDisabled}
required
label="Application ID"
/* cspell:disable-next-line */
helperText="Usually 10 characters, e.g. XYZAAABB00"
/>
</Box>
<Box marginBottom={5} key={"secret"} /* todo why is this "key" here? */>
<Input
control={control}
name="secretKey"
disabled={isFormDisabled}
required
label="Admin API Key"
helperText="In Algolia dashboard it's a masked field"
/>
</Box>
<Input
control={control}
name="indexNamePrefix"
disabled={isFormDisabled}
label="Index name prefix"
helperText='Optional prefix, you can add "test" or "staging" to test the app'
/>
{credentialsValidationError && (
<Box marginTop={5}>
<Text color={"textCriticalDefault"}>
Could not connect to Algolia. Please verify your credentials
</Text>
</Box>
)}
</Box>
<Divider margin={0} marginTop={5} />
<Box paddingX={5} paddingY={3} display={"flex"} justifyContent={"flex-end"}>
<Layout.AppSectionCard
as="form"
onSubmit={onFormSubmit}
footer={
<Box display={"flex"} justifyContent={"flex-end"}>
<Button disabled={isFormDisabled} type="submit" variant="primary">
{isFormDisabled ? "Loading..." : "Save"}
</Button>
</Box>
</form>
</Box>
}
>
<Box>
<Input
control={control}
name="appId"
disabled={isFormDisabled}
required
label="Application ID"
helperText="Usually 10 characters, e.g. XYZAAABB00"
/>
<Box marginBottom={5} key={"secret"} /* todo why is this "key" here? */>
<Input
control={control}
name="secretKey"
disabled={isFormDisabled}
required
label="Admin API Key"
helperText="In Algolia dashboard it's a masked field"
/>
</Box>
<Input
control={control}
name="indexNamePrefix"
disabled={isFormDisabled}
label="Index name prefix"
helperText='Optional prefix, you can add "test" or "staging" to test the app'
/>
{credentialsValidationError && (
<Box marginTop={5}>
<Text color={"textCriticalDefault"}>
Could not connect to Algolia. Please verify your credentials
</Text>
</Box>
)}
</Box>
</Layout.AppSectionCard>
);
};

View file

@ -8,6 +8,7 @@ import {
import { Controller, useForm } from "react-hook-form";
import { useEffect } from "react";
import { useDashboardNotification } from "@saleor/apps-shared";
import { ButtonsBox, Layout } from "@saleor/apps-ui";
export const AlgoliaFieldsSelectionForm = () => {
const { notifySuccess } = useDashboardNotification();
@ -35,46 +36,46 @@ export const AlgoliaFieldsSelectionForm = () => {
}
return (
<Box>
<form
onSubmit={handleSubmit((values) => {
const selectedValues = Object.entries(values)
.filter(([key, selected]) => selected)
.map(([key]) => key);
<Layout.AppSectionCard
as="form"
onSubmit={handleSubmit((values) => {
const selectedValues = Object.entries(values)
.filter(([key, selected]) => selected)
.map(([key]) => key);
mutate({
enabledAlgoliaFields: selectedValues,
});
})}
>
<Box padding={5}>
{AlgoliaRootFieldsKeys.map((field) => (
<Box key={field} marginBottom={5}>
<Controller
name={field}
control={control}
render={({ field: { value, onChange } }) => {
return (
<Checkbox
onCheckedChange={(v) => {
onChange(v);
}}
checked={value}
name={field}
>
{AlgoliaRootFieldsLabelsMap[field]}
</Checkbox>
);
}}
/>
</Box>
))}
</Box>
<Divider margin={0} marginTop={5} />
<Box padding={5} display="flex" justifyContent="flex-end">
mutate({
enabledAlgoliaFields: selectedValues,
});
})}
footer={
<ButtonsBox>
<Button type="submit">Save</Button>
</Box>
</form>
</Box>
</ButtonsBox>
}
>
<Box>
{AlgoliaRootFieldsKeys.map((field) => (
<Box key={field} marginBottom={5}>
<Controller
name={field}
control={control}
render={({ field: { value, onChange } }) => {
return (
<Checkbox
onCheckedChange={(v) => {
onChange(v);
}}
checked={value}
name={field}
>
{AlgoliaRootFieldsLabelsMap[field]}
</Checkbox>
);
}}
/>
</Box>
))}
</Box>
</Layout.AppSectionCard>
);
};

View file

@ -1,36 +0,0 @@
import { Box, PropsWithBox, Text } from "@saleor/macaw-ui/next";
import { ReactNode } from "react";
// todo move to shared
export const AppSection = ({
heading,
sideContent,
mainContent,
includePadding = false,
...props
}: PropsWithBox<{
heading: string;
sideContent?: ReactNode;
mainContent: ReactNode;
includePadding?: boolean;
}>) => {
return (
<Box as="section" __gridTemplateColumns={"400px auto"} display={"grid"} gap={10} {...props}>
<Box>
<Text as="h2" variant={"heading"} size={"large"} marginBottom={1.5}>
{heading}
</Text>
{sideContent}
</Box>
<Box
borderStyle={"solid"}
borderColor={"neutralPlain"}
borderWidth={1}
padding={includePadding ? 5 : 0}
borderRadius={4}
>
{mainContent}
</Box>
</Box>
);
};

View file

@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
import { Products, useQueryAllProducts } from "./useQueryAllProducts";
import { trpcClient } from "../modules/trpc/trpc-client";
import { Layout } from "@saleor/apps-ui";
const BATCH_SIZE = 100;
@ -63,7 +64,19 @@ export const ImportProductsToAlgolia = () => {
}, [searchProvider, currentProductIndex, isAlgoliaImporting, products]);
return (
<Box __cursor={started ? "wait" : "auto"}>
<Layout.AppSectionCard
footer={
searchProvider &&
algoliaConfigured && (
<Box display={"flex"} justifyContent={"flex-end"}>
<Button disabled={started || !searchProvider} onClick={importProducts}>
Start importing
</Button>
</Box>
)
}
__cursor={started ? "wait" : "auto"}
>
{searchProvider && algoliaConfigured ? (
<Box>
<Text variant={"heading"} as={"p"} marginBottom={1.5}>
@ -75,11 +88,6 @@ export const ImportProductsToAlgolia = () => {
<Text marginBottom={5} variant={"bodyStrong"}>
Do not close the app - its running client-side
</Text>
<Box display={"flex"} justifyContent={"flex-end"}>
<Button disabled={started || !searchProvider} onClick={importProducts}>
Start importing
</Button>
</Box>
</Box>
) : (
<Box>
@ -112,7 +120,7 @@ export const ImportProductsToAlgolia = () => {
/>
</div>
)}
</Box>
</Layout.AppSectionCard>
);
};

View file

@ -1,6 +1,6 @@
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { TextLink } from "@saleor/apps-ui";
import { Layout, TextLink } from "@saleor/apps-ui";
import { useIndicesSetupMutation } from "../lib/useIndicesSetup";
import { trpcClient } from "../modules/trpc/trpc-client";
@ -12,7 +12,19 @@ export const IndicesSettings = () => {
algoliaConfiguration?.appConfig?.appId && algoliaConfiguration?.appConfig?.secretKey;
return (
<Box>
<Layout.AppSectionCard
footer={
<Box display={"flex"} justifyContent={"flex-end"}>
<Button
disabled={!isConfigured}
onClick={() => updateWebhooksMutation.mutate()}
variant="primary"
>
Update indices configuration
</Button>
</Box>
}
>
<Box>
<Text variant={"heading"} as={"p"} marginBottom={1.5}>
Performing this operation will update indices to use recommended settings:
@ -60,16 +72,7 @@ export const IndicesSettings = () => {
Please note - if indices are already configured, this operation will overwrite settings
mentioned above.
</Text>
<Box display={"flex"} justifyContent={"flex-end"}>
<Button
disabled={!isConfigured}
onClick={() => updateWebhooksMutation.mutate()}
variant="primary"
>
Update indices configuration
</Button>
</Box>
</Box>
</Box>
</Layout.AppSectionCard>
);
};

View file

@ -2,5 +2,6 @@ import debugPkg from "debug";
/**
* todo rewrite to pino logger
* @deprecated
*/
export const createDebug = (namespace: string) => debugPkg.debug(`app-search:${namespace}`);

View file

@ -1,15 +0,0 @@
/**
* Extracts the app's url from headers from the response.
*/
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
const { host, "x-forwarded-proto": xForwardedProto = "http" } = headers;
const xForwardedProtos = Array.isArray(xForwardedProto)
? xForwardedProto.join(",")
: xForwardedProto;
const protocols = xForwardedProtos.split(",");
// prefer https over other protocols
const protocol = protocols.find((el) => el === "https") || protocols[0];
return `${protocol}://${host}`;
};

View file

@ -1,25 +0,0 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui/next";
import { useEffect } from "react";
// todo move to shared
export function ThemeSynchronizer() {
const { appBridgeState } = useAppBridge();
const { setTheme } = useTheme();
useEffect(() => {
if (!setTheme || !appBridgeState?.theme) {
return;
}
if (appBridgeState.theme === "light") {
setTheme("defaultLight");
}
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
return null;
}

View file

@ -34,7 +34,9 @@ export const configurationRouter = router({
*/
const data = await fetchLegacyConfiguration(settingsManager, domain);
config.setAlgoliaSettings(data);
if (data) {
config.setAlgoliaSettings(data);
}
return config.getConfig();
}

View file

@ -7,12 +7,18 @@ import { AppConfigurationFields } from "./configuration";
export const fetchLegacyConfiguration = async (
settingsManager: SettingsManager,
domain: string,
) => {
const data: AppConfigurationFields = {
secretKey: (await settingsManager.get("secretKey", domain)) || "",
appId: (await settingsManager.get("appId", domain)) || "",
indexNamePrefix: (await settingsManager.get("indexNamePrefix", domain)) || "",
};
): Promise<AppConfigurationFields | null> => {
const secretKey = await settingsManager.get("secretKey", domain);
const appId = await settingsManager.get("appId", domain);
const indexNamePrefix = await settingsManager.get("indexNamePrefix", domain);
return data;
if (secretKey && appId) {
return {
appId,
secretKey,
indexNamePrefix,
};
}
return null;
};

View file

@ -3,12 +3,10 @@ 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 { GraphQLProvider } from "../providers/GraphQLProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NoSSRWrapper } from "@saleor/apps-shared";
import { GraphQLProvider, NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared";
import { trpcClient } from "../modules/trpc/trpc-client";
/**

View file

@ -7,9 +7,8 @@ import {
} from "../../domain/WebhookActivityToggler.service";
import { createLogger } from "../../lib/logger";
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { createGraphQLClient } from "@saleor/apps-shared";
import { createGraphQLClient, getAppBaseUrl } from "@saleor/apps-shared";
import { Client } from "urql";
import { getBaseUrl } from "../../lib/getBaseUrl";
import { isConfigured } from "../../lib/algolia/is-configured";
const logger = createLogger({
@ -53,7 +52,7 @@ export const recreateWebhooksHandlerFactory =
logger.debug(settings, "fetched settings");
const baseUrl = getBaseUrl(req.headers);
const baseUrl = getAppBaseUrl(req.headers);
const enableWebhooks = isConfigured({
configuration: {
appId: appId,

View file

@ -1,21 +0,0 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { PropsWithChildren } from "react";
import { Provider } from "urql";
import { createGraphQLClient } from "@saleor/apps-shared";
export function GraphQLProvider(props: PropsWithChildren<{}>) {
const { appBridgeState } = useAppBridge();
const saleorApiUrl = appBridgeState?.saleorApiUrl!;
if (!appBridgeState?.saleorApiUrl) {
return <div {...props}></div>;
}
const client = createGraphQLClient({
saleorApiUrl,
token: appBridgeState.token,
});
return <Provider value={client} {...props} />;
}

View file

@ -1,5 +1,5 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import { AppSection } from "../../components/AppSection";
import { Layout } from "@saleor/apps-ui";
import { AlgoliaConfigurationForm } from "../../components/AlgoliaConfigurationForm";
import { ImportProductsToAlgolia } from "../../components/ImportProductsToAlgolia";
import { WebhooksStatus } from "../../components/WebhooksStatus";
@ -20,17 +20,20 @@ export const ConfigurationView = () => {
</Text>
<MainInstructions marginTop={1.5} />
</Box>
<AppSection
<Layout.AppSection
includePadding
heading="Webhooks status"
sideContent={<WebhooksStatusInstructions />}
mainContent={<WebhooksStatus />}
/>
>
<Layout.AppSectionCard>
<WebhooksStatus />
</Layout.AppSectionCard>
</Layout.AppSection>
<AppSection
<Layout.AppSection
includePadding={false}
marginTop={14}
heading="Algolia settings"
mainContent={<AlgoliaConfigurationForm />}
sideContent={
<Box>
<Text as="p" marginBottom={1.5}>
@ -44,11 +47,14 @@ export const ConfigurationView = () => {
</Text>
</Box>
}
/>
<AppSection
>
<AlgoliaConfigurationForm />
</Layout.AppSection>
<Layout.AppSection
includePadding={false}
marginTop={14}
heading="Fields filtering"
mainContent={<AlgoliaFieldsSelectionForm />}
heading="Algolia fields filtering"
sideContent={
<Box>
<Text as="p" marginBottom={1.5}>
@ -60,30 +66,35 @@ export const ConfigurationView = () => {
</Text>
</Box>
}
/>
<AppSection
>
<AlgoliaFieldsSelectionForm />
</Layout.AppSection>
<Layout.AppSection
includePadding
marginTop={14}
heading="Index products"
mainContent={<ImportProductsToAlgolia />}
sideContent={
<Box>
<Text>Perform initial index of all products in your Saleor database</Text>
</Box>
}
/>
>
<ImportProductsToAlgolia />
</Layout.AppSection>
<AppSection
<Layout.AppSection
includePadding
marginTop={14}
heading="Set indices settings"
mainContent={<IndicesSettings />}
sideContent={
<Box>
<Text>Sets up indices with recommended settings.</Text>
</Box>
}
/>
>
<IndicesSettings />
</Layout.AppSection>
</Box>
);
};

View file

@ -17,7 +17,8 @@ const nextConfig = () => {
return {
reactStrictMode: true,
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
// TODO Infer names dynamically from disk
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw", "@saleor/trpc"],
};
};

View file

@ -18,6 +18,7 @@
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@saleor/react-hook-form-macaw": "workspace:*",
"@saleor/trpc": "workspace:*",
"@segment/analytics-node": "^1.1.0",
"@sentry/nextjs": "7.55.2",
"@tanstack/react-query": "^4.29.19",

View file

@ -4,11 +4,9 @@ import { RootConfig } from "../schemas/root-config.schema";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { TextLink } from "@saleor/apps-ui";
import { ButtonsBox, Layout, SkeletonLayout, TextLink } from "@saleor/apps-ui";
import { Text } from "@saleor/macaw-ui/next";
import { ButtonsBox } from "@/modules/ui/buttons-box";
import { trpcClient } from "@/modules/trpc/trpc-client";
import { Skeleton } from "@/modules/ui/skeleton";
import { useDashboardNotification } from "@saleor/apps-shared";
const Schema = RootConfig.Schema.unwrap();
@ -22,7 +20,15 @@ const SegmentConfigFormBase = (props: { values: Shape; onSubmit(values: Shape):
});
return (
<Box as="form" onSubmit={handleSubmit(props.onSubmit)}>
<Layout.AppSectionCard
footer={
<ButtonsBox>
<Button type="submit">Save</Button>
</ButtonsBox>
}
as="form"
onSubmit={handleSubmit(props.onSubmit)}
>
<Input
control={control}
name="segmentWriteKey"
@ -41,10 +47,7 @@ const SegmentConfigFormBase = (props: { values: Shape; onSubmit(values: Shape):
</Text>
}
/>
<ButtonsBox marginTop={6}>
<Button type="submit">Save</Button>
</ButtonsBox>
</Box>
</Layout.AppSectionCard>
);
};
@ -63,7 +66,7 @@ export const SegmentConfigForm = () => {
});
if (isLoading) {
return <Skeleton.Section />;
return <SkeletonLayout.Section />;
}
return (

View file

@ -1,42 +1,13 @@
import { httpBatchLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { createHttpBatchLink } from "@saleor/trpc";
import { appBridgeInstance } from "../../pages/_app";
import { AppRouter } from "./trpc-app-router";
function getBaseUrl() {
if (typeof window !== "undefined") return "";
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const trpcClient = createTRPCNext<AppRouter>({
config({ ctx }) {
config() {
return {
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers() {
const { token, saleorApiUrl } = appBridgeInstance?.getState() || {};
if (!token || !saleorApiUrl) {
console.error(
"Can't initialize tRPC client before establishing the App Bridge connection",
);
throw new Error("Token and Saleor API URL unknown");
}
return {
/**
* Attach headers from app to client requests, so tRPC can add them to context
*/
[SALEOR_AUTHORIZATION_BEARER_HEADER]: appBridgeInstance?.getState().token,
[SALEOR_API_URL_HEADER]: appBridgeInstance?.getState().saleorApiUrl,
};
},
}),
],
links: [createHttpBatchLink(appBridgeInstance)],
queryClientConfig: { defaultOptions: { queries: { refetchOnWindowFocus: false } } },
};
},

View file

@ -1,5 +1,5 @@
import { initTRPC } from "@trpc/server";
import { TrpcContext } from "./trpc-context";
import { TrpcContext } from "@saleor/trpc";
import { Permission } from "@saleor/app-sdk/types";
import { ZodError } from "zod";

View file

@ -1,43 +0,0 @@
import { Box, PropsWithBox, Text } from "@saleor/macaw-ui/next";
import { ReactNode } from "react";
// todo move to shared
export const AppSection = ({
heading,
sideContent,
mainContent,
includePadding = true,
...props
}: PropsWithBox<{
heading: string;
sideContent?: ReactNode;
mainContent: ReactNode;
includePadding?: boolean;
}>) => {
return (
<Box
as="section"
__gridTemplateColumns={"400px auto"}
display={"grid"}
gap={10}
__maxWidth={"1200px"}
{...props}
>
<Box>
<Text as="h2" variant={"heading"} size={"large"} marginBottom={1.5}>
{heading}
</Text>
{sideContent}
</Box>
<Box
borderStyle={"solid"}
borderColor={"neutralPlain"}
borderWidth={1}
padding={includePadding ? 5 : 0}
borderRadius={4}
>
{mainContent}
</Box>
</Box>
);
};

View file

@ -1,5 +0,0 @@
import { BoxProps, Box } from "@saleor/macaw-ui/next";
export const ButtonsBox = (props: BoxProps) => {
return <Box display={"flex"} justifyContent="flex-end" gap={4} {...props} />;
};

View file

@ -1,32 +0,0 @@
import { Box, BoxProps } from "@saleor/macaw-ui/next";
/*
* TODO: Make it more generic, move to shared or contribute to macaw
* todo use macaw skeleton
*/
const Section = (props: BoxProps) => {
return (
<Box display="grid" gap={2} {...props}>
<Box
__height="10px"
backgroundColor="surfaceNeutralHighlight"
borderRadius={2}
__width="50%"
/>
<Box
__height="10px"
backgroundColor="surfaceNeutralHighlight"
borderRadius={2}
__width="70%"
/>
<Box
__height="10px"
backgroundColor="surfaceNeutralHighlight"
borderRadius={2}
__width="60%"
/>
</Box>
);
};
export const Skeleton = { Section };

View file

@ -1,11 +1,10 @@
import { GraphQLProvider } from "@/modules/graphql/GraphQLProvider";
import { ThemeSynchronizer } from "@/modules/theme/theme-synchronizer";
import "@saleor/macaw-ui/next/style";
import { trpcClient } from "@/modules/trpc/trpc-client";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import { NoSSRWrapper } from "@saleor/apps-shared";
import { GraphQLProvider, NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import "@saleor/macaw-ui/next/style";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AppProps } from "next/app";

View file

@ -1,6 +1,7 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { createTrpcContext } from "../../../modules/trpc/trpc-context";
import { appRouter } from "../../../modules/trpc/trpc-app-router";
import { createTrpcContext } from "@saleor/trpc";
export default trpcNext.createNextApiHandler({
router: appRouter,

View file

@ -1,7 +1,7 @@
import { SegmentConfigForm } from "@/modules/configuration/segment-config-form/segment-config-form";
import { AppHeader } from "@/modules/ui/app-header";
import { AppSection } from "@/modules/ui/app-section";
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { Layout } from "@saleor/apps-ui";
import { Box, Text } from "@saleor/macaw-ui/next";
import { NextPage } from "next";
@ -19,12 +19,13 @@ const ConfigurationPage: NextPage = () => {
return (
<Box>
<AppHeader />
<AppSection
<Layout.AppSection
marginBottom={14}
heading="Segment.io configuration"
sideContent={<Text>Provide Segment credentials to allow sending events.</Text>}
mainContent={<SegmentConfigForm />}
/>
>
<SegmentConfigForm />
</Layout.AppSection>
</Box>
);
};

View file

@ -1,5 +1,4 @@
export * from "./src/is-in-iframe";
export * from "./src/macaw-theme-provider/macaw-theme-provider";
export * from "./src/no-ssr-wrapper";
export * from "./src/use-dashboard-notification";
export * from "./src/logger";
@ -7,3 +6,6 @@ export * from "./src/saleor-version-compatibility-validator";
export * from "./src/create-graphql-client";
export * from "./src/metadata-manager";
export * from "./src/editor-js/editor-js-plaintext-renderer";
export * from "./src/theme-synchronizer";
export * from "./src/GraphQLProvider";
export * from "./src/get-app-base-url";

View file

@ -27,7 +27,8 @@
"typescript": "5.1.6",
"urql": "^4.0.4",
"vite": "4.4.8",
"vitest": "0.34.1"
"vitest": "0.34.1",
"zod": "3.21.4"
},
"main": "index.ts",
"peerDependencies": {
@ -37,6 +38,7 @@
"pino-pretty": "^10.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"urql": "^4.0.4"
"urql": "^4.0.4",
"zod": "3.21.4"
}
}

View file

@ -1,8 +1,7 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { PropsWithChildren } from "react";
import { Provider } from "urql";
import { createGraphQLClient } from "@saleor/apps-shared";
import { createGraphQLClient } from "./create-graphql-client";
export function GraphQLProvider(props: PropsWithChildren<{}>) {
const { appBridgeState } = useAppBridge();

View file

@ -1,5 +1,9 @@
// todo move to app sdk
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
/**
* TODO Consume this from App SDK
*/
export const getAppBaseUrl = (headers: {
[name: string]: string | string[] | undefined;
}): string => {
const { host, "x-forwarded-proto": xForwardedProto = "http" } = headers;
const xForwardedProtos = Array.isArray(xForwardedProto)

View file

@ -1,47 +0,0 @@
import {
dark,
light,
SaleorThemeColors,
ThemeProvider as MacawUIThemeProvider,
} from "@saleor/macaw-ui";
import { PropsWithChildren } from "react";
import { Theme } from "@material-ui/core/styles";
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 }>
>;
type OuterProps = PropsWithChildren<{ themeOverrides?: Partial<Theme> }>;
/**
* This is theme provider for old Macaw. Will be removed with Macaw/next
*/
export const MacawThemeProvider = (props: OuterProps) => {
return <ThemeProvider {...props} ssr palettes={palettes} />;
};

4
packages/trpc/.eslintrc Normal file
View file

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["saleor"]
}

2
packages/trpc/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from "./src/context";
export * from "./src/http-batch-link";

View file

@ -0,0 +1,29 @@
{
"name": "@saleor/trpc",
"version": "0.0.0",
"scripts": {
"lint:fix": "eslint --fix ."
},
"devDependencies": {
"@saleor/app-sdk": "0.41.1",
"eslint": "8.46.0",
"eslint-config-saleor": "workspace:*",
"next": "13.4.8",
"typescript": "5.1.6"
},
"main": "index.ts",
"peerDependencies": {
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@tanstack/react-query": "^4.29.19",
"@trpc/client": "10.34.0",
"@trpc/next": "10.34.0",
"@trpc/react-query": "10.34.0",
"@trpc/server": "10.34.0",
"next": "13.3.0",
"pino": "^8.14.1",
"pino-pretty": "^10.0.0",
"react": "18.2.0",
"react-dom": "18.2.0"
}
}

View file

@ -1,22 +1,10 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
import { inferAsyncReturnType } from "@trpc/server";
const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
const { host, "x-forwarded-proto": xForwardedProto = "http" } = headers;
const xForwardedProtos = Array.isArray(xForwardedProto)
? xForwardedProto.join(",")
: xForwardedProto;
const protocols = xForwardedProtos.split(",");
// prefer https over other protocols
const protocol = protocols.find((el) => el === "https") || protocols[0];
return `${protocol}://${host}`;
};
import { getAppBaseUrl } from "@saleor/apps-shared";
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
const baseUrl = getBaseUrl(req.headers);
const baseUrl = getAppBaseUrl(req.headers);
return {
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,

View file

@ -0,0 +1,33 @@
import { AppBridge } from "@saleor/app-sdk/app-bridge";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { httpBatchLink } from "@trpc/client";
function getBaseUrl() {
if (typeof window !== "undefined") return "";
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
// eslint-disable-next-line turbo/no-undeclared-env-vars
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const createHttpBatchLink = (appBridgeInstance?: AppBridge) => {
return httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers() {
const { token, saleorApiUrl } = appBridgeInstance?.getState() || {};
if (!token || !saleorApiUrl) {
console.error("Can't initialize tRPC client before establishing the App Bridge connection");
throw new Error("Token and Saleor API URL unknown");
}
return {
/**
* Attach headers from app to client requests, so tRPC can add them to context
*/
[SALEOR_AUTHORIZATION_BEARER_HEADER]: appBridgeInstance?.getState().token,
[SALEOR_API_URL_HEADER]: appBridgeInstance?.getState().saleorApiUrl,
};
},
});
};

9
packages/trpc/turbo.json Normal file
View file

@ -0,0 +1,9 @@
{
"extends": ["//"],
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"env": ["VERCEL_URL", "PORT"]
}
}
}

View file

@ -1,3 +1,6 @@
export * from "./src/text-link";
export * from "./src/semantic-chip";
export * from "./src/breadcrumbs";
export * from "./src/layout";
export * from "./src/buttons-box";
export * from "./src/skeleton-layout";

View file

@ -1,8 +0,0 @@
import { Box } from "@saleor/macaw-ui/next";
import { PropsWithChildren } from "react";
type ExampleProps = PropsWithChildren<{}>;
export const Example = ({ children }: ExampleProps) => {
return <Box>{children}</Box>;
};

View file

@ -0,0 +1,78 @@
import { Box, PropsWithBox, Text } from "@saleor/macaw-ui/next";
import { ReactNode } from "react";
type AppSectionProps = PropsWithBox<{
heading: ReactNode;
sideContent?: ReactNode;
includePadding?: boolean;
leftColumnWidthPx?: number;
maxWidthPx?: number;
}>;
const AppSectionCard = ({
children,
footer,
includePadding = true,
...props
}: PropsWithBox<{
footer?: ReactNode;
includePadding?: boolean;
}>) => {
return (
<Box
borderStyle={"solid"}
borderColor={"neutralPlain"}
borderWidth={1}
borderRadius={4}
{...props}
>
<Box padding={includePadding ? 5 : 0}>{children}</Box>
{footer && (
<Box
borderTopStyle="solid"
borderTopWidth={1}
borderColor="neutralHighlight"
padding={includePadding ? 5 : 0}
marginTop={5}
>
{footer}
</Box>
)}
</Box>
);
};
const AppSection = ({
heading,
sideContent,
includePadding = true,
leftColumnWidthPx = 400,
maxWidthPx = 1200,
children,
...props
}: AppSectionProps) => {
return (
<Box
as="section"
__gridTemplateColumns={`${leftColumnWidthPx}px auto`}
display={"grid"}
gap={10}
__maxWidth={`${maxWidthPx}px`}
alignItems="start"
{...props}
>
<Box>
<Text as="h2" variant={"heading"} size={"large"} marginBottom={1.5}>
{heading}
</Text>
{sideContent}
</Box>
{children}
</Box>
);
};
export const Layout = {
AppSection,
AppSectionCard,
};

View file

@ -0,0 +1,17 @@
import { Box, BoxProps, Skeleton as MacawSkeleton, SkeletonProps } from "@saleor/macaw-ui/next";
const Section = (props: BoxProps) => {
return (
<Box display="grid" gap={2} {...props}>
<MacawSkeleton __height="16px" __width="50%" />
<MacawSkeleton __height="16px" __width="70%" />
<MacawSkeleton __height="16px" __width="60%" />
</Box>
);
};
const Line = (props: SkeletonProps) => {
return <MacawSkeleton __height="16px" __width="60%" {...props} />;
};
export const SkeletonLayout = { Section, Line };

View file

@ -1188,6 +1188,9 @@ importers:
'@saleor/react-hook-form-macaw':
specifier: workspace:*
version: link:../../packages/react-hook-form-macaw
'@saleor/trpc':
specifier: workspace:*
version: link:../../packages/trpc
'@segment/analytics-node':
specifier: ^1.1.0
version: 1.1.0
@ -1760,6 +1763,58 @@ importers:
vitest:
specifier: 0.34.1
version: 0.34.1(jsdom@20.0.3)
zod:
specifier: 3.21.4
version: 3.21.4
packages/trpc:
dependencies:
'@saleor/apps-shared':
specifier: workspace:*
version: link:../shared
'@tanstack/react-query':
specifier: ^4.29.19
version: 4.29.19(react-dom@18.2.0)(react@18.2.0)
'@trpc/client':
specifier: 10.34.0
version: 10.34.0(@trpc/server@10.34.0)
'@trpc/next':
specifier: 10.34.0
version: 10.34.0(@tanstack/react-query@4.29.19)(@trpc/client@10.34.0)(@trpc/react-query@10.34.0)(@trpc/server@10.34.0)(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
'@trpc/react-query':
specifier: 10.34.0
version: 10.34.0(@tanstack/react-query@4.29.19)(@trpc/client@10.34.0)(@trpc/server@10.34.0)(react-dom@18.2.0)(react@18.2.0)
'@trpc/server':
specifier: 10.34.0
version: 10.34.0
pino:
specifier: ^8.14.1
version: 8.14.1
pino-pretty:
specifier: ^10.0.0
version: 10.0.0
react:
specifier: 18.2.0
version: 18.2.0
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
devDependencies:
'@saleor/app-sdk':
specifier: 0.41.1
version: 0.41.1(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
eslint:
specifier: 8.46.0
version: 8.46.0
eslint-config-saleor:
specifier: workspace:*
version: link:../eslint-config-saleor
next:
specifier: 13.4.8
version: 13.4.8(@babel/core@7.22.11)(react-dom@18.2.0)(react@18.2.0)
typescript:
specifier: 5.1.6
version: 5.1.6
packages/ui:
dependencies:
@ -2939,7 +2994,7 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@ampproject/remapping': 2.2.1
'@babel/code-frame': 7.22.5
'@babel/code-frame': 7.22.10
'@babel/generator': 7.22.9
'@babel/helper-compilation-targets': 7.22.9(@babel/core@7.21.8)
'@babel/helper-module-transforms': 7.22.9(@babel/core@7.21.8)
@ -5704,7 +5759,7 @@ packages:
resolution: {integrity: sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.22.5
'@babel/code-frame': 7.22.10
'@babel/generator': 7.22.9
'@babel/helper-environment-visitor': 7.22.5
'@babel/helper-function-name': 7.22.5
@ -5757,7 +5812,7 @@ packages:
resolution: {integrity: sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.22.5
'@babel/code-frame': 7.22.10
'@babel/generator': 7.22.9
'@babel/helper-environment-visitor': 7.22.5
'@babel/helper-function-name': 7.22.5
@ -10358,7 +10413,7 @@ packages:
/@swc/helpers@0.5.1:
resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==}
dependencies:
tslib: 2.6.1
tslib: 2.6.2
/@tanstack/query-core@4.29.19:
resolution: {integrity: sha512-uPe1DukeIpIHpQi6UzIgBcXsjjsDaLnc7hF+zLBKnaUlh7jFE/A+P8t4cU4VzKPMFB/C970n/9SxtpO5hmIRgw==}
@ -12260,7 +12315,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001503
caniuse-lite: 1.0.30001519
electron-to-chromium: 1.4.482
node-releases: 2.0.13
update-browserslist-db: 1.0.11(browserslist@4.21.9)