Compare commits

..

1 commit

Author SHA1 Message Date
Lukasz Ostrowski
fe8dd73f29 remove CMS v1 app codebase 2023-07-19 12:24:04 +02:00
586 changed files with 7599 additions and 121333 deletions

View file

@ -1,8 +0,0 @@
#changelog
---
"apps": minor
---
### Added
- `apps/emails-and-messages/.env.template`: Described the new environment variable and how it works
- `apps/emails-and-messages/src/saleor-app.ts`: Added case "redis" for switch(AplType), which takes advantage of the [RedisAPL PR](https://github.com/saleor/app-sdk/pull/287) I submitted

View file

@ -8,4 +8,5 @@
## Checklist
- [ ] `.github/dependabot.yaml` is up-to date.
- [ ] I added changesets and [read good practices](/.changeset/README.md).

View file

@ -2,18 +2,110 @@ version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
open-pull-requests-limit: 1
schedule:
interval: "daily"
commit-message:
prefix: "[skip ci]"
groups:
trpc:
patterns:
- "@trpc/*"
codegen:
patterns:
- "@graphql-codegen/*"
rtl:
patterns:
- "@testing-library/*"
# Packages
- package-ecosystem: "npm"
directory: "/packages/eslint-config-saleor"
open-pull-requests-limit: 1
schedule:
interval: "weekly"
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: "/packages/shared"
open-pull-requests-limit: 1
schedule:
interval: "weekly"
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: "/packages/ui"
open-pull-requests-limit: 1
schedule:
interval: "weekly"
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: "/packages/react-hook-form-macaw"
open-pull-requests-limit: 1
schedule:
interval: "weekly"
commit-message:
prefix: "[skip ci]"
# Apps
- package-ecosystem: "npm"
directory: "/apps/cms-v2"
open-pull-requests-limit: 1
schedule:
interval: "weekly"
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: apps/crm
open-pull-requests-limit: 1
schedule:
interval: weekly
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: apps/data-importer
open-pull-requests-limit: 1
schedule:
interval: weekly
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: apps/emails-and-messages
open-pull-requests-limit: 1
schedule:
interval: weekly
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: apps/invoices
open-pull-requests-limit: 1
schedule:
interval: weekly
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: apps/klaviyo
open-pull-requests-limit: 1
schedule:
interval: weekly
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: apps/products-feed
open-pull-requests-limit: 1
commit-message:
prefix: "[skip ci]"
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/search
open-pull-requests-limit: 1
commit-message:
prefix: "[skip ci]"
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/slack
open-pull-requests-limit: 1
commit-message:
prefix: "[skip ci]"
schedule:
interval: weekly
- package-ecosystem: "npm"
open-pull-requests-limit: 1
directory: apps/taxes
commit-message:
prefix: "[skip ci]"
schedule:
interval: weekly

3
.github/labeler.yml vendored
View file

@ -6,6 +6,5 @@
"App: Slack": "apps/slack/**/*"
"App: Taxes": "apps/taxes/**/*"
"App: Emails & Messages": "apps/emails-and-messages/**/*"
"App: CMS": "apps/cms-v2/**/*"
"App: CMS": ["apps/cms-v2/**/*"]
"App: CRM": "apps/crm/**/*"
"App: Segment": "apps/segment/**/*"

View file

@ -6,12 +6,9 @@ on:
jobs:
assign_creator:
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }}
runs-on: ubuntu-latest
steps:
- name: Assign PR to creator
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ github.event.pull_request.html_url }}
CREATOR: ${{ github.event.pull_request.user.login }}
run: gh pr edit "$PR_URL" --add-assignee "$CREATOR"
uses: thomaseizinger/assign-pr-creator-action@v1.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,31 +0,0 @@
name: Changesets
on:
pull_request:
types:
- opened
- labeled
- edited
- synchronize
branches-ignore:
- 'changeset-release/**'
jobs:
changeset_check:
name: Changeset added to the PR
# Adding 'skip changesets' label to the PR will skip this job
if: ${{ !contains( github.event.pull_request.labels.*.name, 'skip changeset') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
# check out full history
fetch-depth: 0
- uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
- uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Changeset added
run: npx changeset status --since origin/main

View file

@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: JamieMason/syncpack-github-action@c145cec44b3731b3fe8e859679e240d6ae011f0f
- uses: JamieMason/syncpack-github-action@0.2.2
continue-on-error: true
with:
package-manager: "pnpm"

View file

@ -1,10 +0,0 @@
name: "Check spelling"
on: # rebuild any PRs and main branch changes
pull_request:
jobs:
spellcheck: # run the action
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: streetsidesoftware/cspell-action@22e32eb3d70acf30e3fc09bd46edc1d30fb2d6db

View file

@ -25,11 +25,11 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 16
- uses: pnpm/action-setup@d882d12c64e032187b2edb46d3a0d003b7a43598
- uses: pnpm/action-setup@v2
name: Install pnpm
- run: pnpm install
- name: Create Release Pull Request
uses: changesets/action@f13b1baaa620fde937751f5d2c3572b9da32af23
uses: changesets/action@v1
id: changesets
with:
title: 🚀 Release apps

View file

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
- uses: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3
with:
node-version: 18

1
.gitignore vendored
View file

@ -43,4 +43,3 @@ apps/**/generated
.sentryclirc
.vscode/
.cspellcache

View file

@ -1 +1 @@
* @saleor/delivery-engineering-js
* @saleor/appstore

View file

@ -16,8 +16,6 @@
<a href="https://saleor.io/blog/">📰 Blog</a>
<span></span>
<a href="https://twitter.com/getsaleor">🐦 Twitter</a>
<span></span>
<a href="https://discord.gg/H52JTZAtSH">💬 Discord</a>
</div>
<div align="center">
@ -48,10 +46,10 @@ This repository serves as a starting point in the exploration of Saleor apps.
In the `apps` folder, you will find the following applications:
- [crm](https://docs.saleor.io/docs/3.x/developer/app-store/apps/crm) - exports customers from Saleor to CRM.
- [cms](https://docs.saleor.io/docs/3.x/developer/app-store/apps/cms) - exports products from Saleor to CMS.
- [cms](./apps/cms) - exports products from Saleor to CMS.
- [data-importer](./apps/data-importer) - import data from CSV to Saleor.
- [emails-and-messages](https://docs.saleor.io/docs/3.x/developer/app-store/apps/emails-and-messages/overview) - notifications and email communication with customers.
- [invoices](https://docs.saleor.io/docs/3.x/developer/app-store/apps/invoices) - generate invoice PDF for each order.
- [emails-and-messages](./apps/emails-and-messages) - notifications and email communication with customers.
- [invoices](./apps/invoices) - generate invoice PDF for each order.
- [klaviyo](./apps/klaviyo) - send Saleor events to Klaviyo, where you can notify the customers.
- [monitoring](./apps/monitoring) - send Saleor logs to 3rd party Monitoring services
- [products-feed](./apps/products-feed) - generate products feed XML
@ -59,11 +57,6 @@ In the `apps` folder, you will find the following applications:
- [slack](./apps/slack) - get notifications on Slack channel from Saleor events.
- [taxes](https://docs.saleor.io/docs/3.x/developer/app-store/apps/taxes) - calculate order and checkout taxes using external services.
## Another official apps
Some of Saleor apps are available in separate repositories:
- [Stripe](https://github.com/saleor/saleor-app-payment-stripe)
## Development
You can find the documentation for saleor/apps on [docs.saleor.io](https://docs.saleor.io/docs/3.x/developer/app-store/development).

View file

@ -1,101 +1,5 @@
# saleor-app-cms-v2
## 2.3.3
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 2.3.2
### Patch Changes
- e8660e8: Extracted UI components and use shared package
- 30140ee: Improved some text typos.
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- @saleor/apps-shared@1.9.0
- @saleor/apps-ui@1.2.0
## 2.3.1
### Patch Changes
- a81f061: Updated Macaw to pre-127
- d9d0f64: Fixed background color of the modal. Previously white background was missing and only blur was applied. Now it is blurred and white with an opacity (in the dark mode it is fixed with dark grey)
- fcc37e7: Remove clsx package from the projects no longer using it.
- Updated dependencies [2a1385b]
- Updated dependencies [a81f061]
- Updated dependencies [fcc37e7]
- @saleor/apps-shared@1.8.1
- @saleor/react-hook-form-macaw@0.2.5
- @saleor/apps-ui@1.1.8
## 2.3.0
### Minor Changes
- 6f1c5c9: Added Payload CMS support.
### Patch Changes
- e9378e7: Fix PayloadCMS form where two inputs were stuck together without a margin. Now, they are placed in two columns
- 6f1c5c9: Fix styling of modal in the dark mode
## 2.2.2
### Patch Changes
- 8b3d961: Updated Macaw UI to pre.118
- c50797e: Extracted MetadataManager creation to factory from shared package
- Updated dependencies [8b3d961]
- Updated dependencies [c50797e]
- @saleor/react-hook-form-macaw@0.2.4
- @saleor/apps-shared@1.8.0
- @saleor/apps-ui@1.1.7
## 2.2.1
### Patch Changes
- 3002354: Added error logging for exceptions thrown at tRPC routes.
## 2.2.0
### Minor Changes
- fe767a4: Changed public (manifest) name app to be "CMS" (previously it was "CMS 2", since two apps existed at the same time)
## 2.1.2
### Patch Changes
- 261957f: Updated dependencies: vite, vitest, eslint, prettier, dotenv, turbo, syncpack, changesets, lint staged
- Updated dependencies [261957f]
- @saleor/react-hook-form-macaw@0.2.3
- @saleor/apps-shared@1.7.6
- @saleor/apps-ui@1.1.6
## 2.1.1
### Patch Changes
- 2fab86b: Updated graphql package to 16.7.1 and @graphql-codegen related dependencies to the latest version.
- aa6fec1: Updated Macaw UI to pre-106
- 4578659: Made Builder.io api keys inputs type of "password" so they are masked now
- Updated dependencies [aa6fec1]
- Updated dependencies [aa6fec1]
- @saleor/react-hook-form-macaw@0.2.2
- @saleor/apps-shared@1.7.5
- @saleor/apps-ui@1.1.5
## 2.1.0
### Minor Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-cms-v2",
"version": "2.3.3",
"version": "2.1.0",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -13,29 +13,28 @@
},
"dependencies": {
"@datocms/cma-client-browser": "2.0.0",
"@hookform/resolvers": "^3.3.1",
"@saleor/app-sdk": "0.43.1",
"@hookform/resolvers": "^3.1.0",
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@saleor/react-hook-form-macaw": "workspace:*",
"@sentry/nextjs": "7.67.0",
"@sentry/nextjs": "7.55.2",
"@tanstack/react-query": "^4.29.19",
"@trpc/client": "10.38.1",
"@trpc/next": "10.38.1",
"@trpc/react-query": "10.38.1",
"@trpc/server": "10.38.1",
"@trpc/client": "10.34.0",
"@trpc/next": "10.34.0",
"@trpc/react-query": "10.34.0",
"@trpc/server": "10.34.0",
"@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.4",
"@vitejs/plugin-react": "4.0.0",
"contentful-management": "10.38.3",
"graphql": "16.7.1",
"graphql": "16.6.0",
"graphql-tag": "^2.12.6",
"jsdom": "^20.0.3",
"next": "13.4.8",
"p-ratelimit": "1.0.1",
"pino": "^8.14.1",
"pino-pretty": "^10.0.0",
"qs": "6.11.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "4.0.10",
@ -43,24 +42,23 @@
"strapi-sdk-js": "2.2.0",
"urql": "^4.0.4",
"usehooks-ts": "^2.9.1",
"vite": "4.4.8",
"vitest": "0.34.1",
"vite": "4.3.9",
"vitest": "0.31.3",
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/cli": "3.2.2",
"@graphql-codegen/introspection": "3.0.1",
"@graphql-codegen/typed-document-node": "3.0.2",
"@graphql-codegen/typescript": "3.0.2",
"@graphql-codegen/typescript-operations": "3.0.2",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/qs": "^6.9.7",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"eslint": "8.46.0",
"eslint": "8.44.0",
"eslint-config-saleor": "workspace:*",
"node-mocks-http": "^1.12.2",
"typescript": "5.1.6"

View file

@ -6,20 +6,26 @@ 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 { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
import { Skeleton } from "../ui/skeleton";
const FormSchema = z.object({
connectionId: z.string().min(7),
});
const EmptyState = () => (
<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>
<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>
);
export const BulkSyncSection = () => {
@ -36,7 +42,7 @@ export const BulkSyncSection = () => {
});
if (!connections || !providers) {
return <SkeletonLayout.Section />;
return <Skeleton.Section />;
}
if (connections.length === 0) {
@ -44,7 +50,7 @@ export const BulkSyncSection = () => {
}
return (
<Layout.AppSectionCard>
<Box>
<Text as="h2" marginBottom={6} variant="heading">
Bulk products synchronization
</Text>
@ -82,6 +88,6 @@ export const BulkSyncSection = () => {
<Button type="submit">Start sync</Button>
</ButtonsBox>
</Box>
</Layout.AppSectionCard>
</Box>
);
};

View file

@ -1,8 +1,10 @@
import { Breadcrumbs, ButtonsBox, Layout } from "@saleor/apps-ui";
import { Breadcrumbs } 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";
@ -12,19 +14,16 @@ import { useDashboardNotification } from "@saleor/apps-shared";
const FetchProductsStep = (props: { onButtonClick(): void }) => {
return (
<Layout.AppSectionCard
footer={
<ButtonsBox>
<Button onClick={props.onButtonClick}>Prefetch products</Button>
</ButtonsBox>
}
>
<Box>
<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>
</Layout.AppSectionCard>
<ButtonsBox>
<Button onClick={props.onButtonClick}>Prefetch products</Button>
</ButtonsBox>
</Box>
);
};
@ -67,7 +66,7 @@ export const BulkSyncView = ({
const { products, finished: saleorProductsFetchFinished } = useFetchAllProducts(
state === "fetching",
connection.channelSlug,
connection.channelSlug
);
const { productsStatusList, setInitialProducts, setItemStatus, finished } =
@ -122,14 +121,9 @@ export const BulkSyncView = ({
]}
/>
<Layout.AppSection
<AppSection
marginBottom={8}
heading="1. Fetch products"
sideContent={
<Text>First pre-fetch all Product Variants from Saleor. Do not close the app.</Text>
}
>
{(() => {
mainContent={(() => {
switch (state) {
case "initial": {
return (
@ -155,29 +149,33 @@ export const BulkSyncView = ({
}
}
})()}
</Layout.AppSection>
heading="1. Fetch products"
sideContent={
<Text>First pre-fetch all Product Variants from Saleor. Do not close the app.</Text>
}
/>
{(state === "fetched" || state === "uploading") && productsStatusList && (
<Layout.AppSection
marginTop={14}
<AppSection
heading="2. Upload to the CMS"
sideContent={<Text>Send listed variants to the CMS</Text>}
>
<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>
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>
}
/>
)}
</Box>
);

View file

@ -57,12 +57,10 @@ export const AddConnectionForm = (props: {
control={control}
name="channelSlug"
label="Channel"
options={
channels?.map((c) => ({
value: c.slug,
label: c.name,
})) ?? []
}
options={channels?.map((c) => ({
value: c.slug,
label: c.name,
}))}
/>
<ArrowRightIcon />
<Select
@ -71,12 +69,10 @@ export const AddConnectionForm = (props: {
control={control}
name="providerId"
label="Provider"
options={
providers?.map((p) => ({
value: p.id,
label: p.configName,
})) ?? []
}
options={providers?.map((p) => ({
value: p.id,
label: p.configName,
}))}
/>
</Box>
{errors.root?.serverError && (

View file

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

View file

@ -1,31 +1,35 @@
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 as="p">
<Text marginBottom={4} 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,
isLoading,
} = trpcClient.channelsProvidersConnection.fetchConnections.useQuery();
const { data: connectionsData, refetch: refetchConnections } =
trpcClient.channelsProvidersConnection.fetchConnections.useQuery();
const { mutate: removeConnection } =
trpcClient.channelsProvidersConnection.removeConnection.useMutation({
@ -41,33 +45,19 @@ export const ChannelProviderConnectionList = () => {
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
if (!providers) {
return <SkeletonLayout.Section />;
return <Skeleton.Section />;
}
const handleDelete = (connectionId: string) => {
removeConnection({ id: connectionId });
};
if (isLoading || !connectionsData) {
return <SkeletonLayout.Section />;
if (!connectionsData) {
return <Text>Loading</Text>;
}
return (
<Layout.AppSectionCard
footer={
providers.length > 0 && (
<ButtonsBox>
<Button
onClick={() => {
setDialogOpen(true);
}}
>
Add connection
</Button>
</ButtonsBox>
)
}
>
<Box>
{dialogOpen && (
<AddConnectionModal
onClose={() => {
@ -89,6 +79,17 @@ export const ChannelProviderConnectionList = () => {
/>
)}
{connectionsData.length > 0 && <ConnectionsList onRemove={handleDelete} />}
</Layout.AppSectionCard>
{connectionsData.length > 0 && (
<ButtonsBox marginTop={6}>
<Button
onClick={() => {
setDialogOpen(true);
}}
>
Add connection
</Button>
</ButtonsBox>
)}
</Box>
);
};

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 { SkeletonLayout } from "@saleor/apps-ui";
import { Skeleton } from "../ui/skeleton";
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 <SkeletonLayout.Section />;
return <Skeleton.Section />;
}
return (

View file

@ -1,12 +1,101 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared";
import { Client } from "urql";
import {
MetadataEntry,
EncryptedMetadataManager,
SettingsManager,
} from "@saleor/app-sdk/settings-manager";
import { Client, gql } from "urql";
import {
FetchAppDetailsDocument,
FetchAppDetailsQuery,
UpdateAppMetadataDocument,
} from "../../../generated/graphql";
gql`
mutation UpdateAppMetadata($id: ID!, $input: [MetadataInput!]!) {
updatePrivateMetadata(id: $id, input: $input) {
item {
privateMetadata {
key
value
}
}
}
}
`;
gql`
query FetchAppDetails {
app {
id
privateMetadata {
key
value
}
}
}
`;
gql`
mutation RemoveMetadata($id: ID!, $keys: [String!]!) {
deletePrivateMetadata(id: $id, keys: $keys) {
errors {
message
}
}
}
`;
export type SimpleGraphqlClient = Pick<Client, "mutation" | "query">;
async function fetchAllMetadata(client: SimpleGraphqlClient): Promise<MetadataEntry[]> {
const { error, data } = await client
.query<FetchAppDetailsQuery>(FetchAppDetailsDocument, {})
.toPromise();
if (error) {
return [];
}
return data?.app?.privateMetadata.map((md) => ({ key: md.key, value: md.value })) || [];
}
async function mutateMetadata(
client: SimpleGraphqlClient,
metadata: MetadataEntry[],
appId: string
) {
const { error: mutationError, data: mutationData } = await client
.mutation(UpdateAppMetadataDocument, {
id: appId,
input: metadata,
})
.toPromise();
if (mutationError) {
throw new Error(`Mutation error: ${mutationError.message}`);
}
return (
mutationData?.updatePrivateMetadata?.item?.privateMetadata.map((md) => ({
key: md.key,
value: md.value,
})) || []
);
}
export const createSettingsManager = (
client: Pick<Client, "query" | "mutation">,
appId: string,
client: SimpleGraphqlClient,
appId: string
): SettingsManager => {
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
return metadataManagerFactory.create(client, appId);
/*
* EncryptedMetadataManager gives you interface to manipulate metadata and cache values in memory.
* We recommend it for production, because all values are encrypted.
* If your use case require plain text values, you can use MetadataManager.
*/
return new EncryptedMetadataManager({
// Secret key should be randomly created for production and set as environment variable
encryptionKey: process.env.SECRET_KEY!,
fetchMetadata: () => fetchAllMetadata(client),
mutateMetadata: (metadata) => mutateMetadata(client, metadata, appId),
});
};

View file

@ -1,27 +0,0 @@
import { PayloadCMS } from "@/modules/providers/payloadcms/payloadcms";
import { z } from "zod";
import { SaleorProviderFieldsMappingSchema } from "./saleor-provider-fields-mapping.schema";
const InputSchema = z.object({
type: z.literal(PayloadCMS.type),
authToken: z.string(),
configName: z.string().min(1),
collectionName: z.string().min(1),
productVariantFieldsMapping: SaleorProviderFieldsMappingSchema,
payloadApiUrl: z.string().url(),
authenticatedUserSlug: z.string(),
});
const FullSchema = InputSchema.extend({
id: z.string(),
});
export namespace PayloadCmsProviderConfig {
export type InputShape = z.infer<typeof InputSchema>;
export type FullShape = z.infer<typeof FullSchema>;
export const Schema = {
Input: InputSchema,
Full: FullSchema,
};
}

View file

@ -4,7 +4,6 @@ import { ContentfulProviderConfig } from "./contentful-provider.schema";
import { BuilderIoProviderConfig } from "./builder-provider.schema";
import { StrapiProviderConfig } from "./strapi-provider.schema";
import { DatocmsProviderConfig } from "./datocms-provider.schema";
import { PayloadCmsProviderConfig } from "./payloadcms-provider.schema";
export namespace ProvidersConfig {
const AnyFull = z.union([
@ -15,7 +14,6 @@ export namespace ProvidersConfig {
DatocmsProviderConfig.Schema.Full,
StrapiProviderConfig.Schema.Full,
BuilderIoProviderConfig.Schema.Full,
PayloadCmsProviderConfig.Schema.Full,
]);
export const Schema = {
@ -25,7 +23,6 @@ export namespace ProvidersConfig {
DatocmsProviderConfig.Schema.Input,
StrapiProviderConfig.Schema.Input,
BuilderIoProviderConfig.Schema.Input,
PayloadCmsProviderConfig.Schema.Input,
]),
AnyFullList: z.array(AnyFull),
};

View file

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

View file

@ -7,7 +7,9 @@ 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, SkeletonLayout, TextLink } from "@saleor/apps-ui";
import { ButtonsBox } from "../../ui/buttons-box";
import { TextLink } from "@saleor/apps-ui";
import { Skeleton } from "@/modules/ui/skeleton";
type FormShape = Omit<BuilderIoProviderConfig.InputShape, "type">;
const FormSchema = BuilderIoProviderConfig.Schema.Input.omit({ type: true });
@ -44,7 +46,6 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
<Box display={"grid"} gap={4} marginY={4}>
<Text variant="heading">Provide connection details</Text>
<Input
type="password"
required
control={control}
name="privateApiKey"
@ -59,7 +60,6 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
}
/>
<Input
type="password"
required
control={control}
name="publicApiKey"
@ -102,7 +102,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
padding={2}
>
<Text variant="caption">Saleor Field</Text>
<Text variant="caption">Builder.io field</Text>
<Text variant="caption">Contentful field</Text>
</Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component
@ -125,7 +125,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
size="small"
control={control}
name={`productVariantFieldsMapping.${saleorField}`}
label="Builder.io Field"
label="Builer.io Field"
/>
</Box>
))}
@ -188,7 +188,7 @@ const EditFormVariant = (props: { configId: string }) => {
},
{
enabled: !!props.configId,
},
}
);
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() {
@ -205,11 +205,11 @@ const EditFormVariant = (props: { configId: string }) => {
});
if (!data) {
return <SkeletonLayout.Section />;
return <Skeleton.Section />;
}
if (data.type !== "builder.io") {
throw new Error("Trying to fill builder.io form with non builder.io data");
throw new Error("Trying to fill builer.io form with non builder.io data");
}
return (

View file

@ -9,7 +9,8 @@ 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, TextLink } from "@saleor/apps-ui";
import { ButtonsBox } from "../../ui/buttons-box";
import { TextLink } from "@saleor/apps-ui";
import { SaleorProviderFieldsMappingKeys } from "@/modules/configuration";
type FormSchema = Omit<ContentfulProviderConfig.InputShape, "type">;
@ -52,7 +53,7 @@ const PureForm = ({
});
notifyError(
"Error",
"Could not fetch content types from Contentful. Please check your credentials.",
"Could not fetch content types from Contentful. Please check your credentials."
);
},
});
@ -75,7 +76,7 @@ const PureForm = ({
});
notifyError(
"Error",
"Could not fetch environments from Contentful. Please check your credentials.",
"Could not fetch environments from Contentful. Please check your credentials."
);
},
});
@ -196,7 +197,7 @@ const PureForm = ({
newTab
size="small"
href={`https://app.contentful.com/spaces/${getValues(
"spaceId",
"spaceId"
)}/settings/environments`}
>
here
@ -246,7 +247,7 @@ const PureForm = ({
<Box marginTop={4}>
<Text as="p" variant="heading" size="small">
Map fields from Saleor to your Contentful schema.
Map fields from Saleor to your contentful schema.
</Text>
<Text as="p" marginTop={2} marginBottom={4}>
All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should
@ -356,7 +357,7 @@ const EditVariant = ({ configId }: { configId: string }) => {
},
{
enabled: !!configId,
},
}
);
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() {

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">;
@ -43,7 +43,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
notifyError(
"Error",
"Could not fetch content types from DatoCMS. Please check your credentials.",
"Could not fetch content types from DatoCMS. Please check your credentials."
);
},
});
@ -61,7 +61,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
notifyError(
"Error",
"Could not fetch content types from DatoCMS. Please check your credentials.",
"Could not fetch content types from DatoCMS. Please check your credentials."
);
},
});
@ -161,7 +161,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
{fieldsData && (
<React.Fragment>
<Text as="p" variant="heading" size="small">
Map fields from Saleor to your DatoCMS schema.
Map fields from Saleor to your contentful schema.
</Text>
<Text as="p" marginTop={2} marginBottom={4}>
All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should
@ -177,7 +177,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
padding={2}
>
<Text variant="caption">Saleor Field</Text>
<Text variant="caption">DatoCMS field</Text>
<Text variant="caption">Contentful field</Text>
</Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component
@ -271,7 +271,7 @@ const EditFormVariant = (props: { configId: string }) => {
},
{
enabled: !!props.configId,
},
}
);
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -1,51 +0,0 @@
import { BulkImportProductFragment } from "../../../../generated/graphql";
import { BulkSyncProcessor, BulkSyncProcessorHooks } from "../../bulk-sync/bulk-sync-processor";
import { PayloadCmsProviderConfig } from "@/modules/configuration/schemas/payloadcms-provider.schema";
import { PayloadCMSClient } from "./payloadcms-client";
// todo CORS or proxy
export class PayloadCmsBulkSyncProcessor implements BulkSyncProcessor {
constructor(private config: PayloadCmsProviderConfig.FullShape) {}
async uploadProducts(
products: BulkImportProductFragment[],
hooks: BulkSyncProcessorHooks,
): Promise<void> {
const client = new PayloadCMSClient();
products.flatMap(
(product) =>
product.variants?.map((variant) => {
if (hooks.onUploadStart) {
hooks.onUploadStart({ variantId: variant.id });
}
return client
.upsertProductVariant({
configuration: this.config,
variant: {
id: variant.id,
name: variant.name,
channelListings: variant.channelListings,
product: {
id: product.id,
name: product.name,
slug: product.slug,
},
},
})
.then((r) => {
if (hooks.onUploadSuccess) {
hooks.onUploadSuccess({ variantId: variant.id });
}
})
.catch((e) => {
if (hooks.onUploadError) {
hooks.onUploadError({ variantId: variant.id, error: e });
}
});
}),
);
}
}

View file

@ -1,172 +0,0 @@
import { createLogger } from "@saleor/apps-shared";
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
import { PayloadCmsProviderConfig } from "@/modules/configuration/schemas/payloadcms-provider.schema";
import { FieldsMapper } from "../fields-mapper";
import qs from "qs";
import { z } from "zod";
type Context = {
configuration: PayloadCmsProviderConfig.FullShape;
variant: WebhookProductVariantFragment;
};
/**
* Client uses REST API with built-in query language
* https://payloadcms.com/docs/queries/overview#rest-queries
*/
export class PayloadCMSClient {
private logger = createLogger({ name: "PayloadCMSClient" });
private mapVariantToPayloadFields({ configuration, variant }: Context) {
const fields = FieldsMapper.mapProductVariantToConfigurationFields({
variant,
configMapping: configuration.productVariantFieldsMapping,
});
return fields;
}
private constructCollectionUrl(config: PayloadCmsProviderConfig.FullShape) {
return `${config.payloadApiUrl}/${config.collectionName}`;
}
getItemsBySaleorVariantId(context: Context) {
const queryString = qs.stringify(
{
where: {
[context.configuration.productVariantFieldsMapping.variantId]: {
equals: context.variant.id,
},
},
},
{
addQueryPrefix: true,
},
);
return fetch(`${this.constructCollectionUrl(context.configuration)}${queryString}`, {
headers: this.getHeaders(context),
}).then((r) => r.json());
}
async deleteProductVariant(context: Context) {
const queryString = qs.stringify(
{
where: {
[context.configuration.productVariantFieldsMapping.variantId]: {
equals: context.variant.id,
},
},
},
{
addQueryPrefix: true,
},
);
try {
const response = await fetch(
this.constructCollectionUrl(context.configuration) + queryString,
{
method: "DELETE",
headers: this.getHeaders(context),
},
);
if (response.status >= 400) {
throw new Error("Error while deleting product variant");
}
} catch (e) {
this.logger.error(e);
throw e;
}
}
private getHeaders(context: Context) {
const headers = new Headers({
"Content-Type": "application/json",
});
/**
* https://payloadcms.com/docs/authentication/config#api-keys
*/
if (
context.configuration.authToken.length > 0 &&
context.configuration.authenticatedUserSlug.length > 0
) {
headers.append(
"Authorization",
`${context.configuration.authenticatedUserSlug} API-Key ${context.configuration.authToken}`,
);
}
return headers;
}
uploadProductVariant(context: Context) {
this.logger.debug("Trying to upload product variant");
return fetch(this.constructCollectionUrl(context.configuration), {
method: "POST",
body: JSON.stringify(this.mapVariantToPayloadFields(context)),
headers: this.getHeaders(context),
})
.then((r) => {
if (r.status >= 400) {
throw new Error(`Error while uploading product variant: ${r.statusText}`);
}
})
.catch((e) => {
this.logger.error(e);
throw e;
});
}
async updateProductVariant({ configuration, variant }: Context) {
this.logger.debug("Trying to update product variant");
const queryString = qs.stringify(
{
where: {
[configuration.productVariantFieldsMapping.variantId]: {
equals: variant.id,
},
},
},
{
addQueryPrefix: true,
},
);
try {
const response = await fetch(this.constructCollectionUrl(configuration) + queryString, {
method: "PATCH",
body: JSON.stringify(this.mapVariantToPayloadFields({ configuration, variant })),
headers: this.getHeaders({ configuration, variant }),
});
if (response.status >= 400) {
throw new Error("Error while updating product variant");
}
} catch (e) {
this.logger.error(e);
throw e;
}
}
async upsertProductVariant(context: Context) {
this.logger.debug("Trying to upsert product variant");
try {
await this.uploadProductVariant(context);
} catch (e) {
this.logger.debug("Failed to upload, will try to update");
await this.updateProductVariant(context);
}
}
}

View file

@ -1,264 +0,0 @@
import { SaleorProviderFieldsMappingKeys } from "@/modules/configuration";
import { PayloadCmsProviderConfig } from "@/modules/configuration/schemas/payloadcms-provider.schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { useDashboardNotification } from "@saleor/apps-shared";
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { Input, Select } from "@saleor/react-hook-form-macaw";
import { useRouter } from "next/router";
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, TextLink } from "@saleor/apps-ui";
type FormShape = Omit<PayloadCmsProviderConfig.InputShape, "type">;
type PureFormProps = {
defaultValues: FormShape;
onSubmit(values: FormShape): void;
onDelete?(): void;
};
/*
* todo react on token change, refresh mutation
*/
const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
const { notifyError } = useDashboardNotification();
const { control, getValues, setValue, watch, handleSubmit, clearErrors, setError } = useForm({
defaultValues: defaultValues,
resolver: zodResolver(PayloadCmsProviderConfig.Schema.Input.omit({ type: true })),
});
return (
<Box
as="form"
display={"grid"}
gap={4}
onSubmit={handleSubmit((vals) => {
onSubmit(vals);
})}
>
<Input
required
control={control}
name="configName"
label="Configuration name"
helperText="Meaningful name that will help you understand it later. E.g. 'staging' or 'prod' "
/>
<Box display={"grid"} gap={4} marginY={4}>
<Text variant="heading">Provide connection details</Text>
<Input
required
control={control}
name="payloadApiUrl"
type="url"
label="API url"
helperText="URL where Payload API is available. By default ends with /api"
/>
<Box
backgroundColor="surfaceNeutralHighlight"
borderColor="neutralHighlight"
borderWidth={1}
borderStyle={"solid"}
padding={4}
borderRadius={4}
>
<Text variant="heading" as="h1" marginBottom={4}>
Authorization
</Text>
<Text marginBottom={2} as="p">
Payload can be configured to have open operations (not recommended) or to require an API
key. Key can be generated per user. To authenticate, you need to provide both user slug
and the key itself.{" "}
</Text>
<TextLink
marginBottom={2}
display="block"
newTab
href="https://payloadcms.com/docs/authentication/config"
>
Read more in Payload docs
</TextLink>
<Text as="p" marginBottom={6}>
If your API is open (e.g. for development purposes) leave both fields empty.
</Text>
<Box display="grid" gap={4} gridTemplateColumns={2}>
<Input
control={control}
name="authenticatedUserSlug"
label="Authenticated user slug"
placeholder="e.g. apps"
/>
<Input control={control} name="authToken" type="password" label="User API Key" />
</Box>
</Box>
</Box>
<Box display={"grid"} gap={4} marginY={4}>
<Text variant="heading">Configure fields mapping</Text>
<Input
label="Collection Slug"
name="collectionName"
control={control}
helperText="Slug of your collection in Payload, e.g. 'saleorVariants'"
/>
<React.Fragment>
<Text as="p" variant="heading" size="small">
Map fields from Saleor to your Payload schema.
</Text>
<Text as="p" marginTop={2} marginBottom={4}>
All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should be
type of <Text variant="bodyStrong">JSON</Text>.
</Text>
<Box
marginBottom={4}
display="grid"
__gridTemplateColumns={"50% 50%"}
borderBottomWidth={1}
borderBottomStyle="solid"
borderColor="neutralHighlight"
padding={2}
>
<Text variant="caption">Saleor Field</Text>
<Text variant="caption">Payload field</Text>
</Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component
<Box
display="grid"
__gridTemplateColumns={"50% 50%"}
padding={2}
key={saleorField}
alignItems="center"
>
<Box>
<Text as="p" variant="bodyStrong">
{printSaleorProductFields(saleorField)}
</Text>
<Text variant="caption">
{saleorField === "channels" ? "JSON field" : "Text field"}
</Text>
</Box>
<Input
size="small"
control={control}
name={`productVariantFieldsMapping.${saleorField}`}
label="CMS Field"
/>
</Box>
))}
</React.Fragment>
</Box>
<ButtonsBox>
{onDelete && (
<Button onClick={onDelete} variant="tertiary">
Delete
</Button>
)}
<Button type="submit">Save</Button>
</ButtonsBox>
</Box>
);
};
const AddFormVariant = () => {
const { push } = useRouter();
const { notifySuccess } = useDashboardNotification();
const { mutate } = trpcClient.providersConfigs.addOne.useMutation({
onSuccess() {
notifySuccess("Success", "Added new configuration");
push("/configuration");
},
});
return (
<PureForm
onSubmit={(values) => {
mutate({
...values,
type: "payloadcms",
});
}}
defaultValues={{
payloadApiUrl: "",
authToken: "",
configName: "",
collectionName: "",
authenticatedUserSlug: "",
productVariantFieldsMapping: {
channels: "",
variantName: "",
productId: "",
productName: "",
productSlug: "",
variantId: "",
},
}}
/>
);
};
const EditFormVariant = (props: { configId: string }) => {
const { push } = useRouter();
const { notifySuccess } = useDashboardNotification();
const { data } = trpcClient.providersConfigs.getOne.useQuery(
{
id: props.configId,
},
{
enabled: !!props.configId,
},
);
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() {
notifySuccess("Success", "Updated configuration");
push("/configuration");
},
});
const { mutate: deleteProvider } = trpcClient.providersConfigs.deleteOne.useMutation({
onSuccess() {
notifySuccess("Success", "Removed configuration");
push("/configuration");
},
});
if (!data) {
return null;
}
if (data.type !== "payloadcms") {
throw new Error("Trying to fill Payload CMS form with non Payload CMS data");
}
return (
<PureForm
onDelete={() => {
deleteProvider({
id: props.configId,
});
}}
onSubmit={(values) => {
mutate({
...values,
type: "payloadcms",
id: props.configId,
});
}}
defaultValues={data}
/>
);
};
export const PayloadCMSConfigForm = {
PureVariant: PureForm,
AddVariant: AddFormVariant,
EditVariant: EditFormVariant,
};

View file

@ -1,69 +0,0 @@
import {
WebhookProductFragment,
WebhookProductVariantFragment,
} from "../../../../generated/graphql";
import { PayloadCmsProviderConfig } from "@/modules/configuration/schemas/payloadcms-provider.schema";
import { createLogger } from "@saleor/apps-shared";
import { ProductWebhooksProcessor } from "../../webhooks-operations/product-webhooks-processor";
import { PayloadCMSClient } from "./payloadcms-client";
/*
* todo error handling
*/
export class PayloadCmsWebhooksProcessor implements ProductWebhooksProcessor {
private client = new PayloadCMSClient();
private logger = createLogger({ name: "PayloadCmsWebhooksProcessor" });
constructor(private providerConfig: PayloadCmsProviderConfig.FullShape) {}
async onProductVariantUpdated(productVariant: WebhookProductVariantFragment): Promise<void> {
this.logger.trace("onProductVariantUpdated called");
await this.client.upsertProductVariant({
configuration: this.providerConfig,
variant: productVariant,
});
}
async onProductVariantCreated(productVariant: WebhookProductVariantFragment): Promise<void> {
this.logger.trace("onProductVariantCreated called");
await this.client.uploadProductVariant({
configuration: this.providerConfig,
variant: productVariant,
});
}
async onProductVariantDeleted(productVariant: WebhookProductVariantFragment): Promise<void> {
this.logger.trace("onProductVariantDeleted called");
await this.client.deleteProductVariant({
configuration: this.providerConfig,
variant: productVariant,
});
}
async onProductUpdated(product: WebhookProductFragment): Promise<void> {
this.logger.trace("onProductUpdated called");
const client = new PayloadCMSClient();
await Promise.all(
(product.variants ?? []).map((variant) => {
return client.upsertProductVariant({
configuration: this.providerConfig,
variant: {
id: variant.id,
name: variant.name,
product: {
id: product.id,
name: product.name,
slug: product.slug,
},
},
});
}),
);
}
}

View file

@ -1,29 +0,0 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import { CMSProviderMeta } from "../cms-provider-meta";
import logo from "./logo.png";
import { TextLink } from "@saleor/apps-ui";
export const PayloadCMS = {
formSideInfo: (
<Box>
<Text as="p" marginBottom={2}>
Configure the Payload CMS integration by providing required information.
</Text>
<Text as="p" marginBottom={2}>
Fields are not validated - ensure you enter correct values.
</Text>
<Text as="p" marginBottom={2}>
Consult{" "}
<TextLink newTab href="https://docs.saleor.io/docs/3.x/developer/app-store/apps/cms">
docs
</TextLink>{" "}
for more information how to set up Payload CMS.
</Text>
</Box>
),
type: "payloadcms" as const,
logoUrl: logo.src,
displayName: "Payload",
description: "Open source, typescript first headless CMS. GraphQL included.",
} satisfies CMSProviderMeta;

View file

@ -1,24 +1,12 @@
import { BuilderIo } from "./builder.io/builder-io";
import { Contentful } from "./contentful/contentful";
import { Datocms } from "./datocms/datocms";
import { PayloadCMS } from "./payloadcms/payloadcms";
import { Strapi } from "./strapi/strapi";
export type CMS =
| typeof Contentful
| typeof Datocms
| typeof Strapi
| typeof BuilderIo
| typeof PayloadCMS;
export type CMS = typeof Contentful | typeof Datocms | typeof Strapi | typeof BuilderIo;
export type CMSType = CMS["type"];
export const cmsTypes = [
Contentful.type,
Datocms.type,
Strapi.type,
BuilderIo.type,
PayloadCMS.type,
] as const;
export const cmsTypes = [Contentful.type, Datocms.type, Strapi.type, BuilderIo.type] as const;
export const CMSProviders = [Contentful, Datocms, Strapi, BuilderIo, PayloadCMS] as const;
export const CMSProviders = [Contentful, Datocms, Strapi, BuilderIo] as const;

View file

@ -22,10 +22,6 @@ import { DatocmsProviderConfig } from "../configuration/schemas/datocms-provider
import { BuilderIo } from "./builder.io/builder-io";
import { BuilderIoWebhooksProcessor } from "./builder.io/builder-io-webhooks-processor";
import { BuilderIoBulkSyncProcessor } from "./builder.io/builder-io-bulk-sync-processor";
import { PayloadCmsBulkSyncProcessor } from "./payloadcms/payloadcms-bulk-sync-processor";
import { PayloadCmsProviderConfig } from "../configuration/schemas/payloadcms-provider.schema";
import { PayloadCMS } from "./payloadcms/payloadcms";
import { PayloadCmsWebhooksProcessor } from "./payloadcms/payloadcms-webhooks-processor";
/**
* Almost-single source of new providers. Every time app will need to resolve a provider, it will use on of these factories.
@ -43,9 +39,9 @@ export const ProvidersResolver = {
case "builder.io": {
return new BuilderIoBulkSyncProcessor(config);
}
case "payloadcms": {
return new PayloadCmsBulkSyncProcessor(config);
}
default:
throw new Error(`Unknown provider`);
}
},
getProviderInputSchema(type: CMSType) {
@ -58,8 +54,9 @@ export const ProvidersResolver = {
return StrapiProviderConfig.Schema.Input;
case "builder.io":
return BuilderIoProviderConfig.Schema.Input;
case "payloadcms":
return PayloadCmsProviderConfig.Schema.Input;
default: {
throw new Error("Failed to build input schema");
}
}
},
getProviderSchema(type: CMSType) {
@ -72,11 +69,12 @@ export const ProvidersResolver = {
return StrapiProviderConfig.Schema.Full;
case "builder.io":
return BuilderIoProviderConfig.Schema.Full;
case "payloadcms":
return PayloadCmsProviderConfig.Schema.Full;
default: {
throw new Error("Failed to build provdier schema");
}
}
},
createProviderMeta(type: CMSType): CMS {
createProviderMeta(type: CMSType | string): CMS {
switch (type) {
case "contentful": {
return Contentful;
@ -90,8 +88,8 @@ export const ProvidersResolver = {
case "builder.io": {
return BuilderIo;
}
case "payloadcms": {
return PayloadCMS;
default: {
throw new Error("Unknown provider");
}
}
},
@ -109,13 +107,13 @@ export const ProvidersResolver = {
case "builder.io": {
return new BuilderIoWebhooksProcessor(config);
}
case "payloadcms": {
return new PayloadCmsWebhooksProcessor(config);
default: {
throw new Error("Failed to build webhook processor.");
}
}
},
getEditProviderFormComponent: (
type: CMSType,
type: CMSType
): ComponentType<{
configId: string;
}> => {
@ -123,37 +121,33 @@ export const ProvidersResolver = {
case "contentful": {
return dynamic(() =>
import("./contentful/contentful-config-form").then(
(module) => module.ContentfulConfigForm.EditVariant,
),
(module) => module.ContentfulConfigForm.EditVariant
)
);
}
case "datocms": {
return dynamic(() =>
import("./datocms/datocms-config-form").then(
(module) => module.DatoCMSConfigForm.EditVariant,
),
(module) => module.DatoCMSConfigForm.EditVariant
)
);
}
case "strapi": {
return dynamic(() =>
import("./strapi/strapi-config-form").then(
(module) => module.StrapiConfigForm.EditVariant,
),
(module) => module.StrapiConfigForm.EditVariant
)
);
}
case "builder.io": {
return dynamic(() =>
import("./builder.io/builder-io-config-form").then(
(module) => module.BuilderIoConfigForm.EditVariant,
),
(module) => module.BuilderIoConfigForm.EditVariant
)
);
}
case "payloadcms": {
return dynamic(() =>
import("./payloadcms/payloadcms-config-form").then(
(module) => module.PayloadCMSConfigForm.EditVariant,
),
);
default: {
throw new Error("Provider form not registered");
}
}
},
@ -162,37 +156,31 @@ export const ProvidersResolver = {
case "contentful": {
return dynamic(() =>
import("./contentful/contentful-config-form").then(
(module) => module.ContentfulConfigForm.AddVariant,
),
(module) => module.ContentfulConfigForm.AddVariant
)
);
}
case "datocms": {
return dynamic(() =>
import("./datocms/datocms-config-form").then(
(module) => module.DatoCMSConfigForm.AddVariant,
),
(module) => module.DatoCMSConfigForm.AddVariant
)
);
}
case "strapi": {
return dynamic(() =>
import("./strapi/strapi-config-form").then(
(module) => module.StrapiConfigForm.AddVariant,
),
import("./strapi/strapi-config-form").then((module) => module.StrapiConfigForm.AddVariant)
);
}
case "builder.io": {
return dynamic(() =>
import("./builder.io/builder-io-config-form").then(
(module) => module.BuilderIoConfigForm.AddVariant,
),
(module) => module.BuilderIoConfigForm.AddVariant
)
);
}
case "payloadcms": {
return dynamic(() =>
import("./payloadcms/payloadcms-config-form").then(
(module) => module.PayloadCMSConfigForm.AddVariant,
),
);
default: {
throw new Error("Provider form not registered");
}
}
},

View file

@ -7,7 +7,8 @@ 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, SkeletonLayout } from "@saleor/apps-ui";
import { ButtonsBox } from "../../ui/buttons-box";
import { Skeleton } from "@/modules/ui/skeleton";
type FormShape = Omit<StrapiProviderConfig.InputShape, "type">;
@ -85,7 +86,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
padding={2}
>
<Text variant="caption">Saleor Field</Text>
<Text variant="caption">Strapi field</Text>
<Text variant="caption">Contentful field</Text>
</Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component
@ -171,7 +172,7 @@ const EditFormVariant = (props: { configId: string }) => {
},
{
enabled: !!props.configId,
},
}
);
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() {
@ -188,7 +189,7 @@ const EditFormVariant = (props: { configId: string }) => {
});
if (!data) {
return <SkeletonLayout.Section />;
return <Skeleton.Section />;
}
if (data.type !== "strapi") {

View file

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

View file

@ -9,7 +9,7 @@ dialog {
.dialog-overlay {
z-index: 1;
background: color-mix(in srgb, var(--mu-colors-background-plain) 80%, transparent);
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(5px);
content: "";
position: fixed;

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 { getAppBaseUrl } from "@saleor/apps-shared";
import { getBaseUrl } from "@/modules/shared/get-base-url";
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
const baseUrl = getAppBaseUrl(req.headers);
const baseUrl = getBaseUrl(req.headers);
return {
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,

View file

@ -0,0 +1,43 @@
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,4 +1,5 @@
import { Box, BoxProps } from "@saleor/macaw-ui/next";
import { forwardRef } from "react";
export const Modal = ({ onClose, ...rest }: { onClose(): void } & BoxProps) => {
return (
@ -11,8 +12,6 @@ export const Modal = ({ onClose, ...rest }: { onClose(): void } & BoxProps) => {
as="dialog"
__maxWidth="400px"
boxShadow={"modal"}
backgroundColor="surfaceNeutralPlain"
color="textNeutralDefault"
open
{...rest}
/>

View file

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

View file

@ -1,8 +1,12 @@
import { ContentfulConfigForm } from "@/modules/providers/contentful/contentful-config-form";
import { DatoCMSConfigForm } from "@/modules/providers/datocms/datocms-config-form";
import { CMSType } from "@/modules/providers/providers-registry";
import { ProvidersResolver } from "@/modules/providers/providers-resolver";
import { StrapiConfigForm } from "@/modules/providers/strapi/strapi-config-form";
import { AppHeader } from "@/modules/ui/app-header";
import { Breadcrumbs, Layout } from "@saleor/apps-ui";
import { AppSection } from "@/modules/ui/app-section";
import { Breadcrumbs } from "@saleor/apps-ui";
import { Box, Text } from "@saleor/macaw-ui/next";
import { NextPage } from "next";
import { useRouter } from "next/router";
@ -12,7 +16,7 @@ const AddProviderPage: NextPage = () => {
const { query } = useRouter();
const provider = useMemo(() => {
return query.type ? ProvidersResolver.createProviderMeta(query.type as CMSType) : null;
return query.type ? ProvidersResolver.createProviderMeta(query.type as string) : null;
}, [query.type]);
if (!provider) return null;
@ -31,7 +35,7 @@ const AddProviderPage: NextPage = () => {
]}
/>
<Layout.AppSection
<AppSection
heading={`Set up ${provider.displayName}`}
sideContent={
<Box>
@ -39,11 +43,8 @@ const AddProviderPage: NextPage = () => {
{provider.formSideInfo && <Box marginTop={6}>{provider.formSideInfo}</Box>}
</Box>
}
>
<Layout.AppSectionCard>
<FormComponent />
</Layout.AppSectionCard>
</Layout.AppSection>
mainContent={<FormComponent />}
/>
</Box>
);
};

View file

@ -31,7 +31,7 @@ export default createManifestHandler({
],
homepageUrl: "https://github.com/saleor/apps",
id: "saleor.app.cms2",
name: "CMS",
name: "CMS 2",
permissions: ["MANAGE_PRODUCTS"],
requiredSaleorVersion: ">=3.10 <4",
supportUrl: "https://github.com/saleor/apps/discussions",

View file

@ -1,18 +1,8 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { createTrpcContext } from "../../../modules/trpc/trpc-context";
import { appRouter } from "../../../modules/trpc/trpc-app-router";
import { createLogger } from "@saleor/apps-shared";
const logger = createLogger({ name: "tRPC error" });
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: createTrpcContext,
onError: ({ path, error }) => {
if (error.code === "INTERNAL_SERVER_ERROR") {
logger.error(error, `${path} returned error:`);
return;
}
logger.debug(error, `${path} returned error:`);
},
});

View file

@ -1,9 +1,10 @@
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();
@ -22,7 +23,7 @@ const BulkSyncPage: NextPage = () => {
},
{
enabled: !!parsedID,
},
}
);
const {
@ -35,7 +36,7 @@ const BulkSyncPage: NextPage = () => {
},
{
enabled: !!connection,
},
}
);
if ((providerFetched && !provider) || (connectionFetched && !connection)) {
@ -44,7 +45,7 @@ const BulkSyncPage: NextPage = () => {
}
if (connectionLoading || providerLoading) {
return <SkeletonLayout.Section />;
return <Skeleton.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 { Layout } from "@saleor/apps-ui";
import { AppSection } from "@/modules/ui/app-section";
import { Box, Text } from "@saleor/macaw-ui/next";
import { NextPage } from "next";
@ -10,7 +10,7 @@ const ConfigurationPage: NextPage = () => {
return (
<Box>
<AppHeader />
<Layout.AppSection
<AppSection
marginBottom={14}
heading="Providers configuration"
sideContent={
@ -18,10 +18,9 @@ const ConfigurationPage: NextPage = () => {
<Text>Configure one or more CMS providers to synchronize Saleor products.</Text>
</Box>
}
>
<ProvidersList />
</Layout.AppSection>
<Layout.AppSection
mainContent={<ProvidersList />}
/>
<AppSection
marginBottom={14}
heading="Automatic synchronization"
sideContent={
@ -32,10 +31,9 @@ const ConfigurationPage: NextPage = () => {
</Text>
</Box>
}
>
<ChannelProviderConnectionList />
</Layout.AppSection>
<Layout.AppSection
mainContent={<ChannelProviderConnectionList />}
/>
<AppSection
heading="Initial sync"
sideContent={
<Box>
@ -46,9 +44,8 @@ const ConfigurationPage: NextPage = () => {
<Text as="p">Its recommended to run this flow initially, once app is configured.</Text>
</Box>
}
>
<BulkSyncSection />
</Layout.AppSection>
mainContent={<BulkSyncSection />}
/>
</Box>
);
};

View file

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

@ -1,3 +1 @@
import { vi } from "vitest";
vi.stubEnv("SECRET_KEY", "TEST");
export {};

View file

@ -1,66 +1,5 @@
# saleor-app-crm
## 1.7.8
### Patch Changes
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.7.7
### Patch Changes
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- @saleor/apps-shared@1.9.0
## 1.7.6
### Patch Changes
- a81f061: Updated Macaw to pre-127
- fcc37e7: Remove clsx package from the projects no longer using it.
- Updated dependencies [2a1385b]
- Updated dependencies [a81f061]
- Updated dependencies [fcc37e7]
- @saleor/apps-shared@1.8.1
## 1.7.5
### Patch Changes
- 8b3d961: Updated Macaw UI to pre.118
- Updated dependencies [8b3d961]
- Updated dependencies [c50797e]
- @saleor/apps-shared@1.8.0
## 1.7.4
### Patch Changes
- 3002354: Added error logging for exceptions thrown at tRPC routes.
## 1.7.3
### Patch Changes
- 261957f: Updated dependencies: vite, vitest, eslint, prettier, dotenv, turbo, syncpack, changesets, lint staged
- Updated dependencies [261957f]
- @saleor/apps-shared@1.7.6
## 1.7.2
### Patch Changes
- 2fab86b: Updated graphql package to 16.7.1 and @graphql-codegen related dependencies to the latest version.
- aa6fec1: Updated Macaw UI to pre-106
- Updated dependencies [aa6fec1]
- @saleor/apps-shared@1.7.5
## 1.7.1
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-crm",
"version": "1.7.8",
"version": "1.7.1",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -13,19 +13,19 @@
},
"dependencies": {
"@mailchimp/mailchimp_marketing": "^3.0.80",
"@saleor/app-sdk": "0.43.1",
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@sentry/nextjs": "7.67.0",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@sentry/nextjs": "7.55.2",
"@tanstack/react-query": "4.29.19",
"@trpc/client": "10.38.1",
"@trpc/next": "10.38.1",
"@trpc/react-query": "10.38.1",
"@trpc/server": "10.38.1",
"@trpc/client": "10.34.0",
"@trpc/next": "10.34.0",
"@trpc/react-query": "10.34.0",
"@trpc/server": "10.34.0",
"@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.4",
"@vitejs/plugin-react": "4.0.0",
"clsx": "^1.2.1",
"graphql": "16.7.1",
"graphql": "16.6.0",
"graphql-tag": "^2.12.6",
"jsdom": "^20.0.3",
"next": "13.4.8",
@ -37,24 +37,24 @@
"react-is": "^18.2.0",
"urql": "^4.0.4",
"usehooks-ts": "^2.9.1",
"vite": "4.4.8",
"vitest": "0.34.1",
"vite": "4.3.9",
"vitest": "0.31.3",
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/cli": "3.2.2",
"@graphql-codegen/introspection": "3.0.1",
"@graphql-codegen/typed-document-node": "3.0.2",
"@graphql-codegen/typescript": "3.0.2",
"@graphql-codegen/typescript-operations": "3.0.2",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/mailchimp__mailchimp_marketing": "^3.0.7",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"eslint": "8.46.0",
"eslint": "8.44.0",
"eslint-config-saleor": "workspace:*",
"node-mocks-http": "^1.12.2",
"typescript": "5.1.6"

View file

@ -1,6 +1,6 @@
import { Box, Button, Checkbox, Text } from "@saleor/macaw-ui/next";
import { trpcClient } from "../../../trpc/trpc-client";
import { ChangeEvent, ComponentProps, useEffect, useState } from "react";
import { ComponentProps, useEffect, useState } from "react";
import { Section } from "../../../ui/section/section";
import { useDashboardNotification } from "@saleor/apps-shared";
import { MailchimpListPicker } from "../../mailchimp-list-picker/mailchimp-list-picker";
@ -69,8 +69,8 @@ export const WebhookConfiguration = (props: ComponentProps<typeof Box>) => {
return null;
}
const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>) => {
e.target.checked
const handleCheckboxChange = (checked: boolean) => {
checked
? setLocalState({
selected: true,
listId: listsData[0].id,
@ -106,7 +106,7 @@ export const WebhookConfiguration = (props: ComponentProps<typeof Box>) => {
Each time customer is created, it will be added to selected audience list in Mailchimp
</Text>
<Box display="flex" gap={1.5} flexDirection="column">
<Checkbox onChange={handleCheckboxChange} checked={localState.selected}>
<Checkbox onCheckedChange={handleCheckboxChange} checked={localState.selected}>
<Text marginRight="auto">Enable customers sync</Text>
</Checkbox>
<Box display="flex" alignItems="center">

View file

@ -1,18 +1,8 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { createTrpcContext } from "../../../modules/trpc/trpc-context";
import { appRouter } from "../../../modules/trpc/trpc-app-router";
import { createLogger } from "@saleor/apps-shared";
const logger = createLogger({ name: "tRPC error" });
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: createTrpcContext,
onError: ({ path, error }) => {
if (error.code === "INTERNAL_SERVER_ERROR") {
logger.error(error, `${path} returned error:`);
return;
}
logger.debug(error, `${path} returned error:`);
},
});

View file

@ -9,7 +9,6 @@ import {
IMailchimpConfigSettingsManagerV1,
MailchimpConfigType,
} from "../../modules/mailchimp/mailchimp-config-settings-manager";
import { NextApiRequest, NextApiResponse } from "next";
/**
* Mock settings manager. Consider mocking graphQL api instead
@ -49,29 +48,25 @@ describe("CUSTOMER_CREATED webhook", () => {
it("Call Mailchimp client to add customer with properly mapped data and tags", async () => {
const { req, res } = createMocks({});
await customerCreatedHandler(
req as unknown as NextApiRequest,
res as unknown as NextApiResponse,
{
authData: mockAuthData,
payload: {
user: {
id: "user-id",
email: "someuser@gmail.com",
firstName: "John",
lastName: "Doe",
privateMetadata: [
{
key: "mailchimp_tags",
value: JSON.stringify(["tag1"]),
},
],
},
await customerCreatedHandler(req, res, {
authData: mockAuthData,
payload: {
user: {
id: "user-id",
email: "someuser@gmail.com",
firstName: "John",
lastName: "Doe",
privateMetadata: [
{
key: "mailchimp_tags",
value: JSON.stringify(["tag1"]),
},
],
},
event: "CUSTOMER_CREATED",
baseUrl: "localhost:3000",
}
);
},
event: "CUSTOMER_CREATED",
baseUrl: "localhost:3000",
});
return expect(mailchimp_marketing.lists.addListMember).toHaveBeenCalledWith("saleor", {
email_address: "someuser@gmail.com",

View file

@ -1,60 +1,5 @@
# saleor-app-data-importer
## 1.9.7
### Patch Changes
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.9.6
### Patch Changes
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- @saleor/apps-shared@1.9.0
## 1.9.5
### Patch Changes
- a81f061: Updated Macaw to pre-127
- fcc37e7: Remove clsx package from the projects no longer using it.
- Updated dependencies [2a1385b]
- Updated dependencies [a81f061]
- Updated dependencies [fcc37e7]
- @saleor/apps-shared@1.8.1
## 1.9.4
### Patch Changes
- 8b3d961: Updated Macaw UI to pre.118
- Updated dependencies [8b3d961]
- Updated dependencies [c50797e]
- @saleor/apps-shared@1.8.0
## 1.9.3
### Patch Changes
- 261957f: Updated dependencies: vite, vitest, eslint, prettier, dotenv, turbo, syncpack, changesets, lint staged
- Updated dependencies [261957f]
- @saleor/apps-shared@1.7.6
## 1.9.2
### Patch Changes
- 2fab86b: Updated graphql package to 16.7.1 and @graphql-codegen related dependencies to the latest version.
- aa6fec1: Updated Macaw UI to pre-106
- Updated dependencies [aa6fec1]
- @saleor/apps-shared@1.7.5
## 1.9.1
### Patch Changes

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-data-importer",
"version": "1.9.7",
"version": "1.9.1",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -15,14 +15,15 @@
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.43.1",
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@sentry/nextjs": "7.67.0",
"@saleor/macaw-ui": "^0.7.2",
"@sentry/nextjs": "7.55.2",
"@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.4",
"@vitejs/plugin-react": "4.0.0",
"clsx": "^1.2.1",
"dot-object": "^2.1.4",
"graphql": "16.7.1",
"graphql": "16.6.0",
"graphql-tag": "^2.12.6",
"jose": "^4.11.2",
"jsdom": "^20.0.3",
@ -34,25 +35,26 @@
"react-dom": "18.2.0",
"urql": "^4.0.4",
"usehooks-ts": "^2.9.1",
"vite": "4.4.8",
"vitest": "0.34.1",
"vite": "4.3.9",
"vitest": "0.31.3",
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/cli": "3.2.2",
"@graphql-codegen/introspection": "3.0.1",
"@graphql-codegen/schema-ast": "^3.0.1",
"@graphql-codegen/typed-document-node": "3.0.2",
"@graphql-codegen/typescript": "3.0.2",
"@graphql-codegen/typescript-operations": "3.0.2",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/dot-object": "^2.1.2",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"eslint": "8.46.0",
"@vitest/coverage-c8": "^0.28.4",
"eslint": "8.44.0",
"eslint-config-saleor": "workspace:*",
"typescript": "5.1.6"
},

View file

@ -7,12 +7,6 @@ APL=
REST_APL_ENDPOINT=
REST_APL_TOKEN=
# To use Redis as an APL store, set APP_API_BASE_URL and REDIS_URL.
# URL is in format redis[s]://[[username][:password]@][host][:port][/db-number],
# so for example redis://alice:foobared@awesome.redis.server:6380
# For saleor-platform, thats: `redis://redis:6379/1`
REDIS_URL=
APP_LOG_LEVEL=info
# Local development variables. When developped locally with Saleor inside docker, these can be set to:
@ -22,4 +16,3 @@ APP_LOG_LEVEL=info
# https://docs.saleor.io/docs/3.x/developer/extending/apps/local-app-development
APP_IFRAME_BASE_URL=
APP_API_BASE_URL=

View file

@ -1,93 +1,5 @@
# saleor-app-emails-and-messages
## 1.9.10
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.9.9
### Patch Changes
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- @saleor/apps-shared@1.9.0
- @saleor/apps-ui@1.2.0
## 1.9.8
### Patch Changes
- a81f061: Updated Macaw to pre-127
- fcc37e7: Remove clsx package from the projects no longer using it.
- Updated dependencies [2a1385b]
- Updated dependencies [a81f061]
- Updated dependencies [fcc37e7]
- @saleor/apps-shared@1.8.1
- @saleor/react-hook-form-macaw@0.2.5
- @saleor/apps-ui@1.1.8
## 1.9.7
### Patch Changes
- 8b3d961: Updated Macaw UI to pre.118
- Updated dependencies [8b3d961]
- Updated dependencies [c50797e]
- @saleor/react-hook-form-macaw@0.2.4
- @saleor/apps-shared@1.8.0
- @saleor/apps-ui@1.1.7
## 1.9.6
### Patch Changes
- 3002354: Added error logging for exceptions thrown at tRPC routes.
## 1.9.5
### Patch Changes
- b8c5283: Changed capitalization of the name SendGrid in the UI.
## 1.9.4
### Patch Changes
- 261957f: Updated dependencies: vite, vitest, eslint, prettier, dotenv, turbo, syncpack, changesets, lint staged
- Updated dependencies [261957f]
- @saleor/react-hook-form-macaw@0.2.3
- @saleor/apps-shared@1.7.6
- @saleor/apps-ui@1.1.6
## 1.9.3
### Patch Changes
- c07ddb3: Added support for the new event: fulfillment updated.
- 54901f8: Added new fields to the order related payload: attributes, price details, translated names
## 1.9.2
### Patch Changes
- aa6fec1: Fixed badly aligned Toggles. Now icon and label are centered
- 2fab86b: Updated graphql package to 16.7.1 and @graphql-codegen related dependencies to the latest version.
- aa6fec1: Updated Macaw UI to pre-106
- Updated dependencies [aa6fec1]
- Updated dependencies [aa6fec1]
- @saleor/react-hook-form-macaw@0.2.2
- @saleor/apps-shared@1.7.5
- @saleor/apps-ui@1.1.5
## 1.9.1
### Patch Changes
@ -121,7 +33,7 @@
Added message to interface for users with missing permission or Saleor version without the support for this event.
- e1980aa: Added validation for SendGrid events form. Enabling event without a template is no longer allowed to avoid misconfiguration and undelivered emails.
- e1980aa: Added validation for Sendgrid events form. Enabling event without a template is no longer allowed to avoid misconfiguration and undelivered emails.
- 6299e06: Update @saleor/app-sdk to 0.41.0
- bda814b: Fixed issue with SMTP provider not sending emails on some ports.
- 78670ce: Changed formatting in the debugging logs to be more concise.
@ -167,9 +79,9 @@
- a8834a1: Unified graphql version to 16.6
- cce2fbc: Improved layout spacing across the application.
- e106ab9: Added improved descriptions in the SendGrid configuration views.
- e106ab9: Added improved descriptions in the Sendgrid configuration views.
- a8834a1: Unified graphql codegen packages
- e106ab9: Added improved descriptions in the SendGrid configuration views.
- e106ab9: Added improved descriptions in the Sendgrid configuration views.
- a8834a1: Removed unnecessary duplicated dependencies from apps and moved them to shared and root (types, eslint rules)
- a8834a1: Updated dev dependencies - Typescript, Eslint and Turborepo
- 928c727: Updated @saleor/macaw-ui to 0.8.0-pre.95. This version introduces change in spacing scale, so there may be slight changes in spacing
@ -285,7 +197,7 @@
### Minor Changes
- 14ac614: Enable SendGrid support
- 14ac614: Enable Sendgrid support
### Patch Changes

View file

@ -1,11 +1,7 @@
fragment OrderDetails on Order {
id
number
status
languageCodeEnum
userEmail
created
redirectUrl
channel {
slug
}
@ -21,103 +17,28 @@ fragment OrderDetails on Order {
email
firstName
lastName
languageCode
}
billingAddress {
firstName
lastName
companyName
streetAddress1
streetAddress2
city
cityArea
postalCode
country {
country
}
phone
}
shippingAddress {
firstName
lastName
companyName
streetAddress1
streetAddress2
city
cityArea
postalCode
country {
country
}
phone
}
lines {
id
digitalContentUrl {
id
url
}
isShippingRequired
metadata {
key
value
}
privateMetadata {
key
value
}
productName
translatedProductName
variantName
translatedVariantName
productSku
variant {
preorder{
endDate
}
weight{
unit
value
}
attributes {
attribute {
id
name
slug
}
values {
id
name
slug
file {
url
contentType
}
}
}
product {
attributes {
attribute {
id
name
slug
}
values {
id
name
slug
file{
url
contentType
}
}
}
}
}
quantity
quantityFulfilled
taxRate
thumbnail {
url
alt
@ -127,49 +48,12 @@ fragment OrderDetails on Order {
currency
amount
}
net {
currency
amount
}
tax {
currency
amount
}
}
totalPrice {
gross {
currency
amount
}
net {
currency
amount
}
tax {
currency
amount
}
}
unitDiscount {
currency
amount
}
unitDiscountReason
unitDiscountType
unitDiscountValue
undiscountedUnitPrice{
gross {
currency
amount
}
net {
currency
amount
}
tax {
currency
amount
}
}
}
subtotal {
@ -177,58 +61,17 @@ fragment OrderDetails on Order {
amount
currency
}
net {
currency
amount
}
tax {
currency
amount
}
}
shippingPrice {
gross {
amount
currency
}
net {
currency
amount
}
tax {
currency
amount
}
}
total {
gross {
amount
currency
}
net {
currency
amount
}
tax {
currency
amount
}
}
undiscountedTotal {
gross {
amount
currency
}
net {
currency
amount
}
tax {
currency
amount
}
}
displayGrossPrices
isShippingRequired
shippingMethodName
}

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-emails-and-messages",
"version": "1.9.10",
"version": "1.9.1",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -12,25 +12,26 @@
"test": "vitest"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@hookform/resolvers": "^3.1.0",
"@monaco-editor/react": "^4.4.6",
"@saleor/app-sdk": "0.43.1",
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@saleor/react-hook-form-macaw": "workspace:*",
"@sendgrid/client": "^7.7.0",
"@sendgrid/mail": "^7.7.0",
"@sentry/nextjs": "7.67.0",
"@sentry/nextjs": "7.55.2",
"@tanstack/react-query": "4.29.19",
"@trpc/client": "10.38.1",
"@trpc/next": "10.38.1",
"@trpc/react-query": "10.38.1",
"@trpc/server": "10.38.1",
"@trpc/client": "10.34.0",
"@trpc/next": "10.34.0",
"@trpc/react-query": "10.34.0",
"@trpc/server": "10.34.0",
"@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.4",
"dotenv": "^16.3.1",
"graphql": "16.7.1",
"@vitejs/plugin-react": "4.0.0",
"clsx": "^1.2.1",
"dotenv": "^16.0.3",
"graphql": "16.6.0",
"graphql-tag": "^2.12.6",
"handlebars": "^4.7.7",
"html-to-text": "^9.0.3",
@ -46,27 +47,27 @@
"react-is": "^18.2.0",
"urql": "^4.0.4",
"usehooks-ts": "^2.9.1",
"vite": "4.4.8",
"vitest": "0.34.1",
"vite": "4.3.9",
"vitest": "0.31.3",
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/cli": "3.2.2",
"@graphql-codegen/introspection": "3.0.1",
"@graphql-codegen/schema-ast": "^3.0.1",
"@graphql-codegen/typed-document-node": "3.0.2",
"@graphql-codegen/typescript": "3.0.2",
"@graphql-codegen/typescript-operations": "3.0.2",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/html-to-text": "^9.0.0",
"@types/mjml": "^4.7.0",
"@types/nodemailer": "^6.4.7",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"eslint": "8.46.0",
"eslint": "8.44.0",
"eslint-config-saleor": "workspace:*",
"typescript": "5.1.6"
},

View file

@ -13,9 +13,7 @@ export const BasicLayout = ({ children, breadcrumbs, isLoading = false }: BasicL
{breadcrumbs?.length && (
<Breadcrumbs>
{breadcrumbs.map((breadcrumb) => (
<Breadcrumbs.Item key={breadcrumb.name} href={breadcrumb.href}>
{breadcrumb.name}
</Breadcrumbs.Item>
<Breadcrumbs.Item href={breadcrumb.href}>{breadcrumb.name}</Breadcrumbs.Item>
))}
</Breadcrumbs>
)}

View file

@ -1,310 +0,0 @@
import { MessageEventTypes } from "../modules/event-handlers/message-event-types";
// Notify webhook event groups multiple event types under the one webhook. We need to map it to events recognized by the App
export const notifyEventMapping: Record<string, MessageEventTypes> = {
account_confirmation: "ACCOUNT_CONFIRMATION",
account_delete: "ACCOUNT_DELETE",
account_password_reset: "ACCOUNT_PASSWORD_RESET",
account_change_email_request: "ACCOUNT_CHANGE_EMAIL_REQUEST",
account_change_email_confirm: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
order_fulfillment_update: "ORDER_FULFILLMENT_UPDATE",
};
interface IssuingPrincipal {
id?: string;
type?: string;
}
interface Meta {
issued_at: string;
version: string;
issuing_principal: IssuingPrincipal;
}
export type NotifySubscriptionPayload = {
meta: Meta;
} & (
| {
notify_event: "account_confirmation";
payload: NotifyPayloadAccountConfirmation;
}
| {
notify_event: "account_delete";
payload: NotifyPayloadAccountDelete;
}
| {
notify_event: "account_password_reset";
payload: NotifyPayloadAccountPasswordReset;
}
| {
notify_event: "account_change_email";
payload: NotifyPayloadAccountChangeEmailRequest;
}
| {
notify_event: "account_change_email_confirm";
payload: NotifyPayloadAccountChangeEmailConfirmation;
}
| {
notify_event: "order_fulfillment_update";
payload: NotifyPayloadFulfillmentUpdate;
}
);
export interface NotifyPayloadAccountConfirmation {
channel_slug: string;
confirm_url: string;
domain: string;
logo_url: string;
recipient_email: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadAccountDelete {
channel_slug: string;
delete_url: string;
domain: string;
logo_url: string;
recipient_email: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadAccountPasswordReset {
channel_slug: string;
domain: string;
logo_url: string;
recipient_email: string;
reset_url: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadAccountChangeEmailRequest {
channel_slug: string;
domain: string;
logo_url: string;
new_email: string;
old_email: string;
recipient_email: string;
reset_url: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadAccountChangeEmailConfirmation {
channel_slug: string;
domain: string;
logo_url: string;
recipient_email: string;
site_name: string;
token: string;
user: User;
}
export interface NotifyPayloadFulfillmentUpdate {
channel_slug: string;
digital_lines: DigitalLine[];
domain: string;
fulfillment: Fulfillment;
logo_url: string;
order: Order;
physical_lines: PhysicalLine[];
recipient_email: string;
site_name: string;
token: string;
}
interface User {
email: string;
first_name: string;
id: string;
is_active: boolean;
is_staff: boolean;
language_code: string;
last_name: string;
metadata: Metadata | null;
private_metadata: Metadata | null;
}
type Metadata = Record<string, string>;
interface Order {
billing_address: Address;
channel_slug: string;
collection_point_name: string | null;
created: string;
currency: string;
discount_amount: number;
display_gross_prices: boolean;
email: string;
id: string;
language_code: string;
lines: Line[];
metadata: Metadata | null;
number: number;
order_details_url: string;
private_metadata: Metadata | null;
shipping_address: Address;
shipping_method_name: string;
shipping_price_gross_amount: string;
shipping_price_net_amount: string;
status: string;
subtotal_gross_amount: string;
subtotal_net_amount: string;
tax_amount: string;
token: string;
total_gross_amount: string;
total_net_amount: string;
undiscounted_total_gross_amount: string;
undiscounted_total_net_amount: string;
voucher_discount: number | null;
}
interface Line {
currency: string;
digital_url: string | null;
id: string;
is_digital: boolean;
is_shipping_required: boolean;
metadata: Metadata | null;
product_name: string;
product_sku: string;
product_variant_id: string;
product: Product;
quantity_fulfilled: number;
quantity: number;
tax_rate: string;
total_gross_amount: string;
total_net_amount: string;
total_tax_amount: string;
translated_product_name: string;
translated_variant_name: string;
unit_discount_amount: string;
unit_discount_reason: string | null;
unit_discount_type: string;
unit_discount_value: string;
unit_price_gross_amount: string;
unit_price_net_amount: string;
unit_tax_amount: string;
variant_name: string;
variant: Variant;
}
interface Product {
attributes: AttributeWithAssignment[];
first_image: Image;
id: string;
images: Image[];
weight: string;
}
interface Variant {
first_image: Image;
id: string;
images: Image[];
is_preorder: boolean;
preorder_end_date: string | null;
weight: string;
}
interface AttributeWithAssignment {
assignment: AttributeAssignment;
values: AttributeValue[];
}
interface AttributeAssignment {
attribute: Attribute;
}
interface Attribute {
name: string;
slug: string;
}
interface AttributeValue {
file_url: string | null;
name: string;
slug: string | null;
value: string;
}
interface ImageSizeMapping {
"32": string;
"64": string;
"128": string;
"256": string;
"512": string;
"1024": string;
"2048": string;
"4096": string;
}
interface Image {
original: ImageSizeMapping;
}
interface Address {
first_name: string;
last_name: string;
company_name: string;
street_address_1: string;
street_address_2: string;
city: string;
city_area: string;
postal_code: string;
country: string;
country_area: string;
phone: string;
}
interface Fulfillment {
tracking_number: string;
is_tracking_number_url: boolean;
}
interface PhysicalLine {
id: string;
order_line: OrderLine;
quantity: number;
}
interface DigitalLine {
id: string;
order_line: OrderLine;
quantity: number;
}
interface OrderLine {
currency: string;
digital_url: string | null;
id: string;
is_digital: boolean;
is_shipping_required: boolean;
metadata: Metadata | null;
product_name: string;
product_sku: string;
product_variant_id: string;
product: Product;
quantity_fulfilled: number;
quantity: number;
tax_rate: string;
total_gross_amount: string;
total_net_amount: string;
total_tax_amount: string;
translated_product_name: string;
translated_variant_name: string;
unit_discount_amount: string;
unit_discount_reason: string | null;
unit_discount_type: string;
unit_discount_value: string;
unit_price_gross_amount: string;
unit_price_net_amount: string;
unit_tax_amount: string;
variant_name: string;
variant: Variant;
}

View file

@ -28,7 +28,7 @@ const NoExistingConfigurations = () => {
type ProviderType = "sendgrid" | "smtp";
const providerLabels: Record<ProviderType, string> = {
sendgrid: "SendGrid",
sendgrid: "Sendgrid",
smtp: "SMTP",
};

View file

@ -84,16 +84,12 @@ export const UniversalChannelsSection = ({
onValueChange={onChange}
>
<Switch.Item id="1" value="restrict">
<Box display="flex" alignItems="center" gap={1}>
<TableEditIcon size="medium" />
<Text>Include</Text>
</Box>
<TableEditIcon size="medium" />
<Text>Include</Text>
</Switch.Item>
<Switch.Item id="2" value="exclude">
<Box display="flex" alignItems="center" gap={1}>
<ProductsIcons size="medium" />
<Text>Exclude</Text>
</Box>
<ProductsIcons size="medium" />
<Text>Exclude</Text>
</Switch.Item>
</Switch>
)}

View file

@ -9,25 +9,12 @@ import {
InvoiceSentWebhookPayloadFragment,
GiftCardSentWebhookPayloadFragment,
OrderRefundedWebhookPayloadFragment,
LanguageCodeEnum,
OrderStatus,
} from "../../../generated/graphql";
import {
NotifyPayloadAccountChangeEmailRequest,
NotifyPayloadAccountConfirmation,
NotifyPayloadAccountDelete,
NotifyPayloadAccountPasswordReset,
NotifyPayloadFulfillmentUpdate,
} from "../../lib/notify-event-types";
import { NotifyEventPayload } from "../../pages/api/webhooks/notify";
const exampleOrderPayload: OrderDetailsFragment = {
id: "T3JkZXI6NTdiNTBhNDAtYzRmYi00YjQzLWIxODgtM2JhZmRlMTc3MGQ5",
number: "198",
status: OrderStatus.Unfulfilled,
languageCodeEnum: LanguageCodeEnum.En,
created: "",
displayGrossPrices: true,
isShippingRequired: true,
userEmail: "adrian.king@example.com",
metadata: [
{
@ -48,15 +35,9 @@ const exampleOrderPayload: OrderDetailsFragment = {
email: "adrian.king@example.com",
firstName: "Adrian",
lastName: "King",
languageCode: LanguageCodeEnum.En,
},
billingAddress: {
firstName: "Adrian",
lastName: "King",
streetAddress1: "59314 Mary Well Suite 281",
streetAddress2: "",
companyName: "",
cityArea: "",
city: "METROPOLIS",
postalCode: "71653",
country: {
@ -64,12 +45,7 @@ const exampleOrderPayload: OrderDetailsFragment = {
},
},
shippingAddress: {
firstName: "Adrian",
lastName: "King",
streetAddress1: "59314 Mary Well Suite 281",
streetAddress2: "",
companyName: "",
cityArea: "",
city: "METROPOLIS",
postalCode: "71653",
country: {
@ -91,13 +67,27 @@ const exampleOrderPayload: OrderDetailsFragment = {
currency: "USD",
amount: 5,
},
net: {
},
totalPrice: {
gross: {
currency: "USD",
amount: 4,
amount: 5,
},
tax: {
},
},
{
id: "T3JkZXJMaW5lOjVhYmEzMTBkLTZkMzEtNDNlNy1hZjAyLTdlNGUwM2UzYmI4ZA==",
productName: "Code Division T-shirt",
variantName: "L",
quantity: 1,
thumbnail: {
url: "https://placehold.jp/150x150.png",
alt: "",
},
unitPrice: {
gross: {
currency: "USD",
amount: 1,
amount: 5,
},
},
totalPrice: {
@ -105,96 +95,24 @@ const exampleOrderPayload: OrderDetailsFragment = {
currency: "USD",
amount: 5,
},
net: {
currency: "USD",
amount: 4,
},
tax: {
currency: "USD",
amount: 1,
},
},
isShippingRequired: true,
translatedProductName: "Black Hoodie",
translatedVariantName: "XL",
quantityFulfilled: 0,
taxRate: 10,
unitDiscountValue: 0,
unitDiscount: {
amount: 0,
currency: "USD",
},
undiscountedUnitPrice: {
gross: {
currency: "USD",
amount: 5,
},
net: {
currency: "USD",
amount: 4,
},
tax: {
currency: "USD",
amount: 1,
},
},
metadata: [],
privateMetadata: [],
},
],
subtotal: {
gross: {
amount: 5,
currency: "USD",
},
net: {
amount: 4,
currency: "USD",
},
tax: {
amount: 1,
amount: 10,
currency: "USD",
},
},
shippingPrice: {
gross: {
amount: 10,
currency: "USD",
},
net: {
amount: 8,
currency: "USD",
},
tax: {
amount: 2,
currency: "USD",
},
},
undiscountedTotal: {
gross: {
amount: 15,
currency: "USD",
},
net: {
amount: 12,
currency: "USD",
},
tax: {
amount: 3,
amount: 61.62,
currency: "USD",
},
},
total: {
gross: {
amount: 15,
currency: "USD",
},
net: {
amount: 12,
currency: "USD",
},
tax: {
amount: 3,
amount: 71.62,
currency: "USD",
},
},
@ -249,7 +167,7 @@ const invoiceSentPayload: InvoiceSentWebhookPayloadFragment = {
order: exampleOrderPayload,
};
const accountConfirmationPayload: NotifyPayloadAccountConfirmation = {
const accountConfirmationPayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
@ -271,7 +189,7 @@ const accountConfirmationPayload: NotifyPayloadAccountConfirmation = {
logo_url: "",
};
const accountPasswordResetPayload: NotifyPayloadAccountPasswordReset = {
const accountPasswordResetPayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
@ -293,7 +211,7 @@ const accountPasswordResetPayload: NotifyPayloadAccountPasswordReset = {
logo_url: "",
};
const accountChangeEmailRequestPayload: NotifyPayloadAccountChangeEmailRequest = {
const accountChangeEmailRequestPayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
@ -309,15 +227,15 @@ const accountChangeEmailRequestPayload: NotifyPayloadAccountChangeEmailRequest =
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
old_email: "test@example.com1",
new_email: "new.email@example.com1",
reset_url:
"http://example.com/reset?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
redirect_url:
"http://example.com?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
const accountChangeEmailConfirmPayload: NotifyPayloadAccountChangeEmailRequest = {
const accountChangeEmailConfirmPayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
@ -330,18 +248,14 @@ const accountChangeEmailConfirmPayload: NotifyPayloadAccountChangeEmailRequest =
language_code: "en",
},
recipient_email: "user@example.com",
old_email: "old@example.com",
new_email: "new@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
reset_url:
"http://example.com/reset?email=user%40example.com&token=bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
const accountDeletePayload: NotifyPayloadAccountDelete = {
const accountDeletePayload: NotifyEventPayload = {
user: {
id: "VXNlcjoxOTY=",
email: "user@example.com",
@ -363,184 +277,6 @@ const accountDeletePayload: NotifyPayloadAccountDelete = {
logo_url: "",
};
const orderLineMonospaceTeePayloadFragment: NotifyPayloadFulfillmentUpdate["order"]["lines"][0] = {
id: "T3JkZXJMaW5lOjIwMDg4MmMzLWU3NjItNGE0NS05ZjUxLTUyZDAxYTE2ODZjOQ==",
product: {
id: "UHJvZHVjdDoxMzQ=",
attributes: [
{
assignment: {
attribute: {
slug: "material",
name: "Material",
},
},
values: [
{
name: "Cotton",
value: "",
slug: "cotton",
file_url: null,
},
],
},
],
weight: "",
first_image: {
original: {
"32": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/32/",
"64": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/64/",
"128": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/128/",
"256": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/256/",
"512": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/512/",
"1024": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/1024/",
"2048": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/2048/",
"4096": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/4096/",
},
},
images: [
{
original: {
"32": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/32/",
"64": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/64/",
"128": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/128/",
"256": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/256/",
"512": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/512/",
"1024": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/1024/",
"2048": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/2048/",
"4096": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE4/4096/",
},
},
],
},
product_name: "Monospace Tee",
translated_product_name: "Monospace Tee",
variant_name: "S",
variant: {
id: "UHJvZHVjdFZhcmlhbnQ6MzQ4",
weight: "",
is_preorder: false,
preorder_end_date: null,
first_image: {
original: {
"32": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/32/",
"64": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/64/",
"128": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/128/",
"256": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/256/",
"512": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/512/",
"1024": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/1024/",
"2048": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/2048/",
"4096": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/4096/",
},
},
images: [
{
original: {
"32": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/32/",
"64": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/64/",
"128": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/128/",
"256": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/256/",
"512": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/512/",
"1024": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/1024/",
"2048": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/2048/",
"4096": "https://example.eu.saleor.cloud/thumbnail/UHJvZHVjdE1lZGlhOjE3/4096/",
},
},
],
},
translated_variant_name: "S",
product_sku: "328223580",
product_variant_id: "UHJvZHVjdFZhcmlhbnQ6MzQ4",
quantity: 1,
quantity_fulfilled: 1,
currency: "PLN",
unit_price_net_amount: "90.00",
unit_price_gross_amount: "90.00",
unit_tax_amount: "0.00",
total_gross_amount: "90.00",
total_net_amount: "90.00",
total_tax_amount: "0.00",
tax_rate: "0.0000",
is_shipping_required: true,
is_digital: false,
digital_url: null,
unit_discount_value: "0.000",
unit_discount_reason: null,
unit_discount_type: "fixed",
unit_discount_amount: "0.000",
metadata: {},
};
const addressPayloadFragment: NotifyPayloadFulfillmentUpdate["order"]["billing_address"] = {
first_name: "Caitlin",
last_name: "Johnson",
company_name: "",
street_address_1: "8518 Pamela Track Apt. 164",
street_address_2: "",
city: "APRILSHIRE",
city_area: "",
postal_code: "28290",
country: "US",
country_area: "NC",
phone: "",
};
const orderPayloadFragment: NotifyPayloadFulfillmentUpdate["order"] = {
private_metadata: {},
metadata: {},
status: "fulfilled",
language_code: "en",
currency: "PLN",
total_net_amount: "468.68",
undiscounted_total_net_amount: "468.68",
total_gross_amount: "468.68",
undiscounted_total_gross_amount: "468.68",
display_gross_prices: true,
id: "T3JkZXI6MzU4YzcxNTktZmZlYy00ODI3LWI2MzYtYTNmYTEwMTA2MTI5",
token: "358c7159-ffec-4827-b636-a3fa10106129",
number: 231,
channel_slug: "channel-pln",
created: "2023-07-13 10:54:32.527314+00:00",
shipping_price_net_amount: "18.680",
shipping_price_gross_amount: "18.680",
order_details_url: "",
email: "caitlin.johnson@example.com",
subtotal_gross_amount: "450.00",
subtotal_net_amount: "450.00",
tax_amount: "0.00",
lines: [orderLineMonospaceTeePayloadFragment],
billing_address: addressPayloadFragment,
shipping_address: addressPayloadFragment,
shipping_method_name: "FedEx",
collection_point_name: null,
voucher_discount: null,
discount_amount: 0,
};
const fulfillmentPayloadFragment = {
is_tracking_number_url: false,
tracking_number: "1111-1111-1111-1111",
};
const fulfillmentUpdatePayload: NotifyPayloadFulfillmentUpdate = {
fulfillment: fulfillmentPayloadFragment,
order: orderPayloadFragment,
physical_lines: [
{
id: "XXXXXXXX",
order_line: orderLineMonospaceTeePayloadFragment,
quantity: 1,
},
],
digital_lines: [],
recipient_email: "user@example.com",
token: "bmt4kc-d6e379b762697f6aa357527af36bb9f6",
channel_slug: "default-channel",
domain: "demo.saleor.cloud",
site_name: "Saleor e-commerce",
logo_url: "",
};
// TODO: UPDATE WITH BETTER DATA
const giftCardSentPayload: GiftCardSentWebhookPayloadFragment = {
channel: "default_channel",
@ -593,6 +329,5 @@ export const examplePayloads: Record<MessageEventTypes, any> = {
ORDER_CREATED: orderCreatedPayload,
ORDER_FULFILLED: orderFulfilledPayload,
ORDER_FULLY_PAID: orderFullyPaidPayload,
ORDER_FULFILLMENT_UPDATE: fulfillmentUpdatePayload,
ORDER_REFUNDED: orderRefundedPayload,
};

View file

@ -10,7 +10,6 @@ export const messageEventTypes = [
"ORDER_CONFIRMED",
"ORDER_CREATED",
"ORDER_FULFILLED",
"ORDER_FULFILLMENT_UPDATE",
"ORDER_FULLY_PAID",
"ORDER_REFUNDED",
] as const;
@ -29,7 +28,6 @@ export const messageEventTypesLabels: Record<MessageEventTypes, string> = {
ORDER_CONFIRMED: "Order confirmed",
ORDER_CREATED: "Order created",
ORDER_FULFILLED: "Order fulfilled",
ORDER_FULFILLMENT_UPDATE: "Order fulfillment updated",
ORDER_FULLY_PAID: "Order fully paid",
ORDER_REFUNDED: "Order refunded",
};

View file

@ -91,7 +91,7 @@ export const sendEventMessages = async ({
});
if (sendgridStatus?.errors.length) {
logger.error("SendGrid errors");
logger.error("Sendgrid errors");
logger.error(sendgridStatus?.errors);
}
}

View file

@ -45,7 +45,7 @@ export const sendSendgrid = async ({
return;
}
logger.debug("Sending an email using SendGrid");
logger.debug("Sending an email using Sendgrid");
const { template } = eventSettings;
@ -77,7 +77,7 @@ export const sendSendgrid = async ({
});
logger.debug("Email has been send");
} catch (error) {
logger.error("The SendGrid API returned an error");
logger.error("The Sendgrid API returned an error");
logger.error(error);
if (error instanceof Error) {
return { errors: [{ message: error.message }] };

View file

@ -7,7 +7,7 @@ export const fetchTemplates =
async () => {
if (!apiKey) {
console.warn(
"The SendGrid API key has not been set up yet. Skipping fetching available templates."
"The Sendgrid API key has not been set up yet. Skipping fetching available templates."
);
return [];
}
@ -22,7 +22,7 @@ export const fetchTemplates =
);
if (!response.ok) {
console.error("Could not fetch available SendGrid templates");
console.error("Could not fetch available Sendgrid templates");
return [];
}
try {
@ -37,7 +37,7 @@ export const fetchTemplates =
return templates;
} catch (e) {
console.error("Could not parse the response from SendGrid", e);
console.error("Could not parse the response from Sendgrid", e);
return [];
}
};
@ -47,7 +47,7 @@ export const fetchSenders =
async () => {
if (!apiKey) {
console.warn(
"The SendGrid API key has not been set up yet. Skipping fetching available senders ."
"The Sendgrid API key has not been set up yet. Skipping fetching available senders ."
);
return [];
}
@ -59,7 +59,7 @@ export const fetchSenders =
});
if (!response.ok) {
console.error("Could not fetch available SendGrid senders");
console.error("Could not fetch available Sendgrid senders");
return [];
}
try {
@ -76,7 +76,7 @@ export const fetchSenders =
return senders;
} catch (e) {
console.error("Could not parse the response from SendGrid", e);
console.error("Could not parse the response from Sendgrid", e);
return [];
}
};

View file

@ -62,7 +62,7 @@ export const ApiConnectionSection = ({ configuration }: ApiConnectionSectionProp
label="API Key"
name="apiKey"
control={control}
helperText="The API key can be generated in your SendGrid dashboard"
helperText="The API key can be generated in your Sendgrid dashboard"
/>
<label>

View file

@ -3,7 +3,7 @@ import { Text } from "@saleor/macaw-ui/next";
export const SendgridApiKeyDescriptionText = () => (
<Text as="p">
The API keys can be found at your SendGrid dashboard, in the Settings menu. You can find more
The API keys can be found at your Sendgrid dashboard, in the Settings menu. You can find more
information in the{" "}
<TextLink href="https://docs.sendgrid.com/ui/account-and-settings/api-keys" newTab={true}>
documentation

View file

@ -75,7 +75,7 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
title="Events"
description={
<Text as="p">
Choose which Saleor events should send emails via SendGrid. You can create and modify your
Choose which Saleor events should send emails via Sendgrid. You can create and modify your
templates in the{" "}
<TextLink href="https://mc.sendgrid.com/dynamic-templates" newTab={true}>
Sendgrid dashboard

View file

@ -7,5 +7,5 @@ interface SendgridLogoProps {
}
export const SendgridLogo = ({ height, width }: SendgridLogoProps) => {
return <Image alt="SendGrid logo" src={sendgrid} height={height} width={width} />;
return <Image alt="Sendgrid logo" src={sendgrid} height={height} width={width} />;
};

View file

@ -57,7 +57,7 @@ export const SendgridSenderSection = ({ configuration }: SendgridSenderSectionPr
<Text as="p">
Authenticating the sender is required to send emails. Configure your sender in{" "}
<TextLink href="https://app.sendgrid.com/settings/sender_auth" newTab={true}>
SendGrid dashboard
Sendgrid dashboard
</TextLink>{" "}
and choose it from the list.
</Text>

View file

@ -36,42 +36,6 @@ const addressSection = `<mj-section>
</mj-section>
`;
const addressSectionForNotify = `<mj-section>
<mj-column>
<mj-table>
<thead>
<tr>
<th>
Billing address
</th>
<th>
Shipping address
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
{{#if order.billing_address}}
{{ order.billing_address.street_address_1 }}
{{else}}
No billing address
{{/if}}
</td>
<td>
{{#if order.shipping_address}}
{{ order.shipping_address.street_address_1}}
{{else}}
No shipping required
{{/if}}
</td>
</tr>
</tbody>
</mj-table>
</mj-column>
</mj-section>
`;
const orderLinesSection = `<mj-section>
<mj-column>
<mj-table>
@ -131,7 +95,7 @@ const defaultOrderFulfilledMjmlTemplate = `<mjml>
Hello!
</mj-text>
<mj-text>
Order {{ order.number }} has been fulfilled.
Order {{ order.number}} has been fulfilled.
</mj-text>
</mj-column>
</mj-section>
@ -165,7 +129,7 @@ const defaultOrderFullyPaidMjmlTemplate = `<mjml>
Hello!
</mj-text>
<mj-text>
Order {{ order.number }} has been fully paid.
Order {{ order.number}} has been fully paid.
</mj-text>
</mj-column>
</mj-section>
@ -182,7 +146,7 @@ const defaultOrderRefundedMjmlTemplate = `<mjml>
Hello!
</mj-text>
<mj-text>
Order {{ order.number }} has been refunded.
Order {{ order.number}} has been refunded.
</mj-text>
</mj-column>
</mj-section>
@ -199,7 +163,7 @@ const defaultOrderCancelledMjmlTemplate = `<mjml>
Hello!
</mj-text>
<mj-text>
Order {{ order.number }} has been cancelled.
Order {{ order.number}} has been cancelled.
</mj-text>
</mj-column>
</mj-section>
@ -326,27 +290,6 @@ const defaultAccountDeleteMjmlTemplate = `<mjml>
</mj-body>
</mjml>`;
const defaultOrderFulfillmentUpdatedMjmlTemplate = `<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="16px">
Hello!
</mj-text>
<mj-text>
Fulfillment for the order {{ order.number }} has been updated.
</mj-text>
{{#if fulfillment.tracking_number }}
<mj-text>
Tracking number: {{ fulfillment.tracking_number }}
</mj-text>
{{/if}}
</mj-column>
</mj-section>
${addressSectionForNotify}
</mj-body>
</mjml>`;
export const defaultMjmlTemplates: Record<MessageEventTypes, string> = {
ACCOUNT_CHANGE_EMAIL_CONFIRM: defaultAccountChangeEmailConfirmationMjmlTemplate,
ACCOUNT_CHANGE_EMAIL_REQUEST: defaultAccountChangeEmailRequestMjmlTemplate,
@ -359,7 +302,6 @@ export const defaultMjmlTemplates: Record<MessageEventTypes, string> = {
ORDER_CONFIRMED: defaultOrderConfirmedMjmlTemplate,
ORDER_CREATED: defaultOrderCreatedMjmlTemplate,
ORDER_FULFILLED: defaultOrderFulfilledMjmlTemplate,
ORDER_FULFILLMENT_UPDATE: defaultOrderFulfillmentUpdatedMjmlTemplate,
ORDER_FULLY_PAID: defaultOrderFullyPaidMjmlTemplate,
ORDER_REFUNDED: defaultOrderRefundedMjmlTemplate,
};
@ -376,7 +318,6 @@ export const defaultMjmlSubjectTemplates: Record<MessageEventTypes, string> = {
ORDER_CONFIRMED: "Order {{ order.number }} has been confirmed",
ORDER_CREATED: "Order {{ order.number }} has been created",
ORDER_FULFILLED: "Order {{ order.number }} has been fulfilled",
ORDER_FULFILLMENT_UPDATE: "Fulfillment for order {{ order.number }} has been updated",
ORDER_FULLY_PAID: "Order {{ order.number }} has been fully paid",
ORDER_REFUNDED: "Order {{ order.number }} has been refunded",
};

View file

@ -114,7 +114,7 @@ describe("WebhookManagementService", function () {
name: invoiceSentWebhook.name,
targetUrl: "https://example.com/api/webhooks/invoice-sent",
query:
"subscription InvoiceSent { event { ...InvoiceSentWebhookPayload }}fragment InvoiceSentWebhookPayload on InvoiceSent { invoice { id metadata { key value } privateMetadata { key value } message externalUrl url order { id } } order { ...OrderDetails }}fragment OrderDetails on Order { id number status languageCodeEnum userEmail created redirectUrl channel { slug } metadata { key value } privateMetadata { key value } user { email firstName lastName languageCode } billingAddress { firstName lastName companyName streetAddress1 streetAddress2 city cityArea postalCode country { country } phone } shippingAddress { firstName lastName companyName streetAddress1 streetAddress2 city cityArea postalCode country { country } phone } lines { id digitalContentUrl { id url } isShippingRequired metadata { key value } privateMetadata { key value } productName translatedProductName variantName translatedVariantName productSku variant { preorder { endDate } weight { unit value } attributes { attribute { id name slug } values { id name slug file { url contentType } } } product { attributes { attribute { id name slug } values { id name slug file { url contentType } } } } } quantity quantityFulfilled taxRate thumbnail { url alt } unitPrice { gross { currency amount } net { currency amount } tax { currency amount } } totalPrice { gross { currency amount } net { currency amount } tax { currency amount } } unitDiscount { currency amount } unitDiscountReason unitDiscountType unitDiscountValue undiscountedUnitPrice { gross { currency amount } net { currency amount } tax { currency amount } } } subtotal { gross { amount currency } net { currency amount } tax { currency amount } } shippingPrice { gross { amount currency } net { currency amount } tax { currency amount } } total { gross { amount currency } net { currency amount } tax { currency amount } } undiscountedTotal { gross { amount currency } net { currency amount } tax { currency amount } } displayGrossPrices isShippingRequired shippingMethodName}",
"subscription InvoiceSent { event { ...InvoiceSentWebhookPayload }}fragment InvoiceSentWebhookPayload on InvoiceSent { invoice { id metadata { key value } privateMetadata { key value } message externalUrl url order { id } } order { ...OrderDetails }}fragment OrderDetails on Order { id number userEmail channel { slug } metadata { key value } privateMetadata { key value } user { email firstName lastName } billingAddress { streetAddress1 city postalCode country { country } } shippingAddress { streetAddress1 city postalCode country { country } } lines { id productName variantName quantity thumbnail { url alt } unitPrice { gross { currency amount } } totalPrice { gross { currency amount } } } subtotal { gross { amount currency } } shippingPrice { gross { amount currency } } total { gross { amount currency } }}",
},
});
});

View file

@ -42,7 +42,6 @@ export const eventToWebhookMapping: Record<MessageEventTypes, AppWebhook> = {
ORDER_FULFILLED: "orderFulfilledWebhook",
ORDER_FULLY_PAID: "orderFullyPaidWebhook",
ORDER_REFUNDED: "orderRefundedWebhook",
ORDER_FULFILLMENT_UPDATE: "notifyWebhook",
};
const logger = createLogger({

View file

@ -1,4 +1,4 @@
import { AppWebhook } from "./webhook-management-service";
import { AppWebhook, AppWebhooks } from "./webhook-management-service";
export type WebhookStatuses = Record<AppWebhook, boolean>;

View file

@ -1,18 +1,8 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { createTrpcContext } from "../../../modules/trpc/trpc-context";
import { appRouter } from "../../../modules/trpc/trpc-app-router";
import { createLogger } from "@saleor/apps-shared";
const logger = createLogger({ name: "tRPC error" });
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: createTrpcContext,
onError: ({ path, error }) => {
if (error.code === "INTERNAL_SERVER_ERROR") {
logger.error(error, `${path} returned error:`);
return;
}
logger.debug(error, `${path} returned error:`);
},
});

View file

@ -2,13 +2,67 @@ import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handl
import { saleorApp } from "../../../saleor-app";
import { createLogger, createGraphQLClient } from "@saleor/apps-shared";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { NotifySubscriptionPayload, notifyEventMapping } from "../../../lib/notify-event-types";
import { MessageEventTypes } from "../../../modules/event-handlers/message-event-types";
// Notify webhook event groups multiple event types under the one webhook. We need to map it to events recognized by the App
const notifyEventMapping: Record<string, MessageEventTypes> = {
account_confirmation: "ACCOUNT_CONFIRMATION",
account_delete: "ACCOUNT_DELETE",
account_password_reset: "ACCOUNT_PASSWORD_RESET",
account_change_email_request: "ACCOUNT_CHANGE_EMAIL_REQUEST",
account_change_email_confirm: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
};
/*
* The Notify webhook is triggered on multiple Saleor events.
* Type of the message is determined by `notify_event` field in the payload.
* Notify event handles multiple event types which are recognized based on payload field `notify_event`.
* Handler recognizes if event is one of the supported typed and sends appropriate message.
*/
interface NotifySubscriptionPayload {
notify_event: string;
payload: NotifyEventPayload;
meta: Meta;
}
interface Meta {
issued_at: Date;
version: string;
issuing_principal: IssuingPrincipal;
}
interface IssuingPrincipal {
id: null | string;
type: null | string;
}
export interface NotifyEventPayload {
user: User;
recipient_email: string;
channel_slug: string;
domain: string;
site_name: string;
logo_url: string;
token?: string;
confirm_url?: string;
reset_url?: string;
delete_url?: string;
old_email?: string;
new_email?: string;
redirect_url?: string;
}
interface User {
id: string;
email: string;
first_name: string;
last_name: string;
is_staff: boolean;
is_active: boolean;
private_metadata: Record<string, string>;
metadata: Record<string, string>;
language_code: string;
}
export const notifyWebhook = new SaleorAsyncWebhook<NotifySubscriptionPayload>({
name: "notify",
webhookPath: "api/webhooks/notify",
@ -35,10 +89,10 @@ const handler: NextWebhookApiHandler<NotifySubscriptionPayload> = async (req, re
.json({ error: "Email recipient has not been specified in the event payload." });
}
// Since NOTIFY can be send on events unrelated to this app, lack of mapping means the App does not support it
const event = notifyEventMapping[payload.notify_event];
if (!event) {
// NOTIFY webhook sends multiple events to the same endpoint. The app supports only a subset of them.
logger.debug(`The type of received notify event (${payload.notify_event}) is not supported.`);
return res.status(200).json({ message: `${payload.notify_event} event is not supported.` });
}

View file

@ -28,9 +28,9 @@ const ChooseProviderPage: NextPage = () => {
<SectionWithDescription title="Choose provider">
<Box display="grid" gridTemplateColumns={2} gap={3}>
<ProviderSelectionBox
providerName="SendGrid"
providerName="Sendgrid"
providerLogo={<SendgridLogo height={20} width={20} />}
providerDescription="Use dynamic templates created in SendGrid dashboard to send messages. Event data will be forwarded to SendGrid."
providerDescription="Use dynamic templates created in Sendgrid dashboard to send messages. Event data will be forwarded to Sendgrid."
onClick={() => push(sendgridUrls.newConfiguration())}
/>

View file

@ -17,7 +17,7 @@ const LoadingView = () => {
<BasicLayout
breadcrumbs={[
{ name: "Configuration", href: appUrls.configuration() },
{ name: "SendGrid provider" },
{ name: "Sendgrid provider" },
{ name: "..." },
]}
>
@ -31,7 +31,7 @@ const NotFoundView = () => {
<BasicLayout
breadcrumbs={[
{ name: "Configuration", href: appUrls.configuration() },
{ name: "SendGrid provider" },
{ name: "Sendgrid provider" },
{ name: "Not found" },
]}
>
@ -78,13 +78,13 @@ const EditSendgridConfigurationPage: NextPage = () => {
breadcrumbs={[
{ name: "Configuration", href: appUrls.configuration() },
{
name: `SendGrid: ${configuration.name}`,
name: `Sendgrid: ${configuration.name}`,
},
]}
>
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
<Box>
<Text>Connect SendGrid with Saleor.</Text>
<Text>Connect Sendgrid with Saleor.</Text>
</Box>
</Box>
<SendgridBasicInformationSection configuration={configuration} />

View file

@ -43,16 +43,16 @@ const NewSendgridConfigurationPage: NextPage = () => {
breadcrumbs={[
{ name: "Configuration", href: appUrls.configuration() },
{ name: "Add provider" },
{ name: "SendGrid" },
{ name: "Sendgrid" },
]}
>
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
<Box>
<Text>Connect SendGrid with Saleor.</Text>
<Text>Connect Sendgrid with Saleor.</Text>
</Box>
</Box>
<SectionWithDescription
title="Connect SendGrid"
title="Connect Sendgrid"
description={
<Box display="flex" flexDirection="column" gap={2}>
<ConfigurationNameDescriptionText />
@ -80,7 +80,7 @@ const NewSendgridConfigurationPage: NextPage = () => {
name="apiKey"
control={control}
label="API key"
helperText={"The API key can be generated in your SendGrid dashboard"}
helperText={"The API key can be generated in your Sendgrid dashboard"}
/>
</Box>
<BoxFooter>

View file

@ -1,4 +1,4 @@
import { APL, FileAPL, RedisAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
import { APL, FileAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
import { SaleorApp } from "@saleor/app-sdk/saleor-app";
const aplType = process.env.APL ?? "file";
@ -6,12 +6,6 @@ const aplType = process.env.APL ?? "file";
export let apl: APL;
switch (aplType) {
case "redis": {
if (!process.env.REDIS_URL) throw new Error("Missing redis url");
if (!process.env.APP_API_BASE_URL)
throw new Error("Redis relies on APP_API_BASE_URL to store keys, please set env variable");
apl = new RedisAPL(new URL(process.env.REDIS_URL), process.env.APP_API_BASE_URL);
}
case "upstash":
apl = new UpstashAPL();

View file

@ -1,7 +1,5 @@
{
"extends": [
"//"
],
"extends": ["//"],
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
@ -23,8 +21,7 @@
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_ENVIRONMENT",
"APP_IFRAME_BASE_URL",
"APP_API_BASE_URL",
"REDIS_URL"
"APP_API_BASE_URL"
]
}
}

View file

@ -1,72 +1,5 @@
# saleor-app-invoices
## 1.16.0
### Minor Changes
- 4aee4e1: Replace text "loading" messages with skeletons
- 4aee4e1: Redesigned app layout. Now app uses shared sections as other apps.
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 1.15.7
### Patch Changes
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- @saleor/apps-shared@1.9.0
## 1.15.6
### Patch Changes
- a81f061: Updated Macaw to pre-127
- fcc37e7: Remove clsx package from the projects no longer using it.
- Updated dependencies [2a1385b]
- Updated dependencies [a81f061]
- Updated dependencies [fcc37e7]
- @saleor/apps-shared@1.8.1
## 1.15.5
### Patch Changes
- 8b3d961: Updated Macaw UI to pre.118
- Updated dependencies [8b3d961]
- Updated dependencies [c50797e]
- @saleor/apps-shared@1.8.0
## 1.15.4
### Patch Changes
- 3002354: Added error logging for exceptions thrown at tRPC routes.
## 1.15.3
### Patch Changes
- 261957f: Updated dependencies: vite, vitest, eslint, prettier, dotenv, turbo, syncpack, changesets, lint staged
- Updated dependencies [261957f]
- @saleor/apps-shared@1.7.6
## 1.15.2
### Patch Changes
- 2fab86b: Updated graphql package to 16.7.1 and @graphql-codegen related dependencies to the latest version.
- aa6fec1: Updated Macaw UI to pre-106
- Updated dependencies [aa6fec1]
- @saleor/apps-shared@1.7.5
## 1.15.1
### Patch Changes

View file

@ -1,18 +1,13 @@
const { withSentryConfig } = require("@sentry/nextjs");
const isSentryPropertiesInEnvironment = Boolean(
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_PROJECT && process.env.SENTRY_ORG,
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_PROJECT && process.env.SENTRY_ORG
);
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
transpilePackages: [
"@saleor/apps-shared",
"@saleor/apps-ui",
"@saleor/react-hook-form-macaw",
"@saleor/trpc",
],
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
};
const configWithSentry = withSentryConfig(
@ -28,7 +23,9 @@ const configWithSentry = withSentryConfig(
tunnelRoute: "/monitoring",
hideSourceMaps: true,
disableLogger: true,
},
}
);
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;

View file

@ -1,6 +1,6 @@
{
"name": "saleor-app-invoices",
"version": "1.16.0",
"version": "1.15.1",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -12,22 +12,21 @@
"test": "vitest"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@saleor/app-sdk": "0.43.1",
"@hookform/resolvers": "^3.1.0",
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@saleor/trpc": "workspace:*",
"@sentry/nextjs": "7.67.0",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@sentry/nextjs": "7.55.2",
"@tanstack/react-query": "4.29.19",
"@trpc/client": "10.38.1",
"@trpc/next": "10.38.1",
"@trpc/react-query": "10.38.1",
"@trpc/server": "10.38.1",
"@trpc/client": "10.34.0",
"@trpc/next": "10.34.0",
"@trpc/react-query": "10.34.0",
"@trpc/server": "10.34.0",
"@urql/exchange-auth": "^2.1.4",
"@web-std/file": "^3.0.2",
"eslint": "8.46.0",
"graphql": "16.7.1",
"clsx": "^1.2.1",
"eslint": "8.44.0",
"graphql": "16.6.0",
"graphql-tag": "^2.12.6",
"microinvoice": "^1.0.6",
"next": "13.4.8",
@ -42,25 +41,26 @@
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/cli": "3.2.2",
"@graphql-codegen/introspection": "3.0.1",
"@graphql-codegen/schema-ast": "^3.0.1",
"@graphql-codegen/typed-document-node": "3.0.2",
"@graphql-codegen/typescript": "3.0.2",
"@graphql-codegen/typescript-operations": "3.0.2",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"@types/rimraf": "^3.0.2",
"@vitejs/plugin-react": "4.0.4",
"dotenv": "^16.3.1",
"@vitejs/plugin-react": "4.0.0",
"@vitest/coverage-c8": "^0.28.4",
"dotenv": "^16.0.3",
"eslint-config-saleor": "workspace:*",
"jsdom": "^20.0.3",
"rimraf": "^3.0.2",
"typescript": "5.1.6",
"vite": "4.4.8",
"vitest": "0.34.1"
"vite": "4.3.9",
"vitest": "0.31.3"
},
"private": true,
"saleor": {

View file

@ -0,0 +1,25 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui/next";
import { memo, 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

@ -6,7 +6,6 @@ import { SimpleGraphqlClient } from "../metadata-manager";
export const mockMetadataManager = {
set: vi.fn().mockImplementation(async () => {}),
get: vi.fn().mockImplementation(async () => {}),
delete: vi.fn().mockImplementation(async () => {}),
};
export const createSettingsManager = (client: SimpleGraphqlClient): SettingsManager => {

View file

@ -53,8 +53,6 @@ describe("appConfigurationRouter", function () {
token: "TOKEN",
saleorApiUrl: "http://localhost:8000/graphql/",
appId: "app",
ssr: true,
baseUrl: "localhost:3000",
})
.upsertChannelOverride({
channelSlug: "test",

View file

@ -1,14 +1,13 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useDashboardNotification } from "@saleor/apps-shared";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
import React, { useCallback, useEffect } from "react";
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
import { useRouter } from "next/router";
import { useCallback } from "react";
import { z } from "zod";
import { trpcClient } from "../../trpc/trpc-client";
import { SellerAddress } from "../address";
import { trpcClient } from "../../trpc/trpc-client";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useDashboardNotification } from "@saleor/apps-shared";
import { useRouter } from "next/router";
import { AddressV2Schema, AddressV2Shape } from "../schema-v2/app-config-schema.v2";
type Props = {
@ -58,29 +57,12 @@ export const AddressForm = (props: Props & InnerFormProps) => {
});
return (
<Layout.AppSectionCard
as="form"
footer={
<ButtonsBox>
<Button
variant="tertiary"
onClick={(e) => {
e.stopPropagation();
props.onCancel();
}}
>
<Text color={"textNeutralSubdued"}>Cancel</Text>
</Button>
<Button type="submit" variant="primary">
Save
</Button>
</ButtonsBox>
}
<form
onSubmit={handleSubmit((data, event) => {
return props.onSubmit(data);
})}
>
<Box display={"grid"} gap={3}>
<Box display={"grid"} gap={3} marginBottom={9}>
{fieldsBlock1.map((fieldName) => (
<Controller
key={fieldName}
@ -146,7 +128,21 @@ export const AddressForm = (props: Props & InnerFormProps) => {
/>
))}
</Box>
</Layout.AppSectionCard>
<Box display={"grid"} justifyContent={"flex-end"} gap={1.5} gridAutoFlow={"column"}>
<Button
variant="tertiary"
onClick={(e) => {
e.stopPropagation();
props.onCancel();
}}
>
<Text color={"textNeutralSubdued"}>Cancel</Text>
</Button>
<Button type="submit" variant="primary">
Save
</Button>
</Box>
</form>
);
};
@ -168,6 +164,9 @@ export const ConnectedAddressForm = (props: Props) => {
const { push } = useRouter();
const addressData =
channelOverrideConfigQuery.data && channelOverrideConfigQuery.data[props.channelSlug];
const submitHandler = useCallback(
async (data: AddressV2Shape) => {
return upsertConfigMutation.mutate({
@ -175,7 +174,7 @@ export const ConnectedAddressForm = (props: Props) => {
channelSlug: props.channelSlug,
});
},
[props.channelSlug, upsertConfigMutation],
[props.channelSlug, upsertConfigMutation]
);
const onCancelHandler = useCallback(() => {
@ -183,7 +182,7 @@ export const ConnectedAddressForm = (props: Props) => {
}, [push]);
if (channelOverrideConfigQuery.isLoading) {
return <SkeletonLayout.Section />;
return <Text color={"textNeutralSubdued"}>Loading</Text>;
}
return (

View file

@ -1,25 +1,29 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import { DefaultShopAddress } from "../../shop-info/ui/default-shop-address";
import { AppSection } from "../../ui/AppSection";
import { PerChannelConfigList } from "../../channels/ui/per-channel-config-list";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { Layout } from "@saleor/apps-ui";
export const AppConfigView = () => {
const { appBridge } = useAppBridge();
return (
<Box>
<Text as={"h1"} variant={"hero"} marginBottom={5}>
Configuration
</Text>
<Text as={"p"} marginBottom={1.5}>
The Invoices App will generate invoices for each order, for which{" "}
<code>INVOICE_REQUESTED</code> event will be triggered
</Text>
<Layout.AppSection
marginTop={10}
heading={"Default address of the shop"}
sideContent={
<Box
display={"grid"}
justifyContent={"space-between"}
__gridTemplateColumns={"400px 400px"}
gap={10}
__marginBottom={"200px"}
>
<Box>
<Text as={"h1"} variant={"hero"} marginBottom={5}>
Configuration
</Text>
<Text as={"p"} marginBottom={1.5}>
The Invoices App will generate invoices for each order, for which{" "}
<code>INVOICE_REQUESTED</code> event will be triggered
</Text>
<Text as={"p"} marginBottom={1.5}>
By default it will use{" "}
<a
@ -28,7 +32,7 @@ export const AppConfigView = () => {
appBridge?.dispatch(
actions.Redirect({
to: "/site-settings",
}),
})
);
}}
>
@ -36,25 +40,22 @@ export const AppConfigView = () => {
</a>{" "}
address, but each channel can be configured separately
</Text>
}
>
<DefaultShopAddress />
</Layout.AppSection>
<Layout.AppSection
marginTop={10}
</Box>
<Box>
<DefaultShopAddress />
</Box>
</Box>
<AppSection
includePadding={true}
heading={"Shop address per channel"}
mainContent={<PerChannelConfigList />}
sideContent={
<Text>
Configure custom billing address for each channel. If not set, default shop address will
be used
</Text>
}
>
<Layout.AppSectionCard>
<PerChannelConfigList />
</Layout.AppSectionCard>
</Layout.AppSection>
/>
</Box>
);
};

View file

@ -1,9 +1,9 @@
import { Box, ChevronRightIcon, Text, Button } from "@saleor/macaw-ui/next";
import { AppSection } from "../../ui/AppSection";
import { useRouter } from "next/router";
import { ConnectedAddressForm } from "../ui/address-form";
import { trpcClient } from "../../trpc/trpc-client";
import { useDashboardNotification } from "@saleor/apps-shared";
import { Layout } from "@saleor/apps-ui";
export const ChannelConfigView = () => {
const {
@ -15,7 +15,7 @@ export const ChannelConfigView = () => {
const { notifySuccess } = useDashboardNotification();
if (!channel) {
return null; // TODO: error
return null;
}
return (
@ -29,9 +29,10 @@ export const ChannelConfigView = () => {
<Text>{channel}</Text>
</Box>
</Box>
<Layout.AppSection
<AppSection
includePadding={true}
heading={"Shop address per channel"}
mainContent={<ConnectedAddressForm channelSlug={channel as string} />}
sideContent={
<Box>
<Text marginBottom={5} as={"p"}>
@ -50,9 +51,7 @@ export const ChannelConfigView = () => {
</Button>
</Box>
}
>
<ConnectedAddressForm channelSlug={channel as string} />
</Layout.AppSection>
/>
</Box>
);
};

View file

@ -1,7 +1,6 @@
import { Box, Text, Chip, Button } from "@saleor/macaw-ui/next";
import { trpcClient } from "../../trpc/trpc-client";
import { useRouter } from "next/router";
import { SkeletonLayout } from "@saleor/apps-ui";
const defaultAddressChip = (
<Chip __display={"inline-block"} size={"large"}>
@ -18,7 +17,7 @@ export const PerChannelConfigList = () => {
const { push } = useRouter();
if (shopChannelsQuery.isLoading || channelsOverridesQuery.isLoading) {
return <SkeletonLayout.Section />;
return <Text color={"textNeutralSubdued"}>Loading...</Text>;
}
const renderChannelAddress = (slug: string) => {

View file

@ -1,23 +1,16 @@
import { describe, it, expect, vi } from "vitest";
import { hashInvoiceFilename } from "./hash-invoice-filename";
vi.mock("crypto", async () => {
const actual = (await vi.importActual("crypto")) as Crypto;
return {
default: {
...actual,
randomUUID() {
return "RANDOM_UUID_MOCK";
},
},
};
});
vi.mock("crypto", () => ({
randomUUID() {
return "RANDOM_UUID_MOCK";
},
}));
describe("hashInvoiceFilename", () => {
it("Creates hashed invoice name", () => {
expect(hashInvoiceFilename("1/12/2022", "1234-xxxx-zzzz-1234")).toBe(
"1/12/2022_1234-xxxx-zzzz-1234_RANDOM_UUID_MOCK",
"1/12/2022_1234-xxxx-zzzz-1234_RANDOM_UUID_MOCK"
);
});
});

View file

@ -2,31 +2,30 @@ import { Box, Text, Button } from "@saleor/macaw-ui/next";
import { trpcClient } from "../../trpc/trpc-client";
import { PropsWithChildren } from "react";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
const Wrapper = ({ children }: PropsWithChildren<{}>) => {
const { appBridge } = useAppBridge();
return (
<Layout.AppSectionCard
footer={
<ButtonsBox>
<Button
onClick={() => {
appBridge?.dispatch(
actions.Redirect({
to: "/site-settings",
}),
);
}}
>
Edit
</Button>
</ButtonsBox>
}
>
<Box>
<Box display={"flex"} justifyContent={"space-between"} marginBottom={5}>
<Text variant={"heading"}>Default address of the shop</Text>
<Button
size={"small"}
variant={"tertiary"}
onClick={() => {
appBridge?.dispatch(
actions.Redirect({
to: "/site-settings",
})
);
}}
>
<Text color={"textNeutralSubdued"}>Edit</Text>
</Button>
</Box>
<Box>{children}</Box>
</Layout.AppSectionCard>
</Box>
);
};
@ -47,7 +46,7 @@ export const DefaultShopAddress = () => {
if (isLoading) {
return (
<Wrapper>
<SkeletonLayout.Section />
<Text color={"textNeutralSubdued"}>Loading...</Text>
</Wrapper>
);
}
@ -71,9 +70,6 @@ export const DefaultShopAddress = () => {
if (data && data.companyAddress) {
return (
<Wrapper>
<Text as="p" marginBottom={4} variant="caption">
This address will be used if custom address is not set for channel
</Text>
<Text size={"small"} as={"p"}>
{data.companyAddress.companyName}
</Text>

Some files were not shown because too many files have changed in this diff Show more