Add data-importer app
This commit is contained in:
parent
f867f2e2db
commit
073ce83e30
48 changed files with 134114 additions and 0 deletions
2
apps/data-importer/.env.example
Normal file
2
apps/data-importer/.env.example
Normal file
|
@ -0,0 +1,2 @@
|
|||
# https://getnuvo.com/
|
||||
NEXT_PUBLIC_NUVO_LICENSE_KEY=
|
3
apps/data-importer/.eslintrc
Normal file
3
apps/data-importer/.eslintrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["next", "prettier"]
|
||||
}
|
20
apps/data-importer/.graphqlrc.yml
Normal file
20
apps/data-importer/.graphqlrc.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
schema: graphql/schema.graphql
|
||||
documents: [graphql/**/*.graphql, src/**/*.ts, src/**/*.tsx]
|
||||
extensions:
|
||||
codegen:
|
||||
overwrite: true
|
||||
generates:
|
||||
generated/graphql.ts:
|
||||
config:
|
||||
dedupeFragments: true
|
||||
plugins:
|
||||
- typescript
|
||||
- typescript-operations
|
||||
- urql-introspection
|
||||
- typescript-urql:
|
||||
documentVariablePrefix: "Untyped"
|
||||
fragmentVariablePrefix: "Untyped"
|
||||
- typed-document-node
|
||||
generated/schema.graphql:
|
||||
plugins:
|
||||
- schema-ast
|
1
apps/data-importer/.npmrc
Normal file
1
apps/data-importer/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
strict-peer-dependencies=false
|
1
apps/data-importer/.nvmrc
Normal file
1
apps/data-importer/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
v16.13.0
|
5
apps/data-importer/.prettierignore
Normal file
5
apps/data-importer/.prettierignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
.next
|
||||
saleor/api.tsx
|
||||
pnpm-lock.yaml
|
||||
graphql/schema.graphql
|
||||
generated
|
4
apps/data-importer/.prettierrc
Normal file
4
apps/data-importer/.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": false,
|
||||
"printWidth": 100
|
||||
}
|
1
apps/data-importer/CODEOWNERS
Normal file
1
apps/data-importer/CODEOWNERS
Normal file
|
@ -0,0 +1 @@
|
|||
* @saleor/appstore
|
37
apps/data-importer/LICENSE
Normal file
37
apps/data-importer/LICENSE
Normal file
|
@ -0,0 +1,37 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020-2022, Saleor Commerce
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
-------
|
||||
|
||||
Unless stated otherwise, artwork included in this distribution is licensed
|
||||
under the Creative Commons Attribution 4.0 International License.
|
||||
|
||||
You can learn more about the permitted use by visiting
|
||||
https://creativecommons.org/licenses/by/4.0/
|
144
apps/data-importer/README.md
Normal file
144
apps/data-importer/README.md
Normal file
|
@ -0,0 +1,144 @@
|
|||

|
||||
|
||||
<div align="center">
|
||||
<h1>Saleor App Template</h1>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<p>Bare-bones boilerplate for writing Saleor Apps with Next.js.</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://saleor.io/">🏠 Website</a>
|
||||
<span> • </span>
|
||||
<a href="https://docs.saleor.io/docs/3.x/">📚 Docs</a>
|
||||
<span> • </span>
|
||||
<a href="https://saleor.io/blog/">📰 Blog</a>
|
||||
<span> • </span>
|
||||
<a href="https://twitter.com/getsaleor">🐦 Twitter</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://githubbox.com/saleor/saleor-app-template">🔎 Explore Code</a>
|
||||
</div>
|
||||
|
||||
## About
|
||||
|
||||
### What is Saleor App
|
||||
|
||||
Saleor App is the fastest way of extending Saleor with custom logic using [asynchronous](https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks) and [synchronous](https://docs.saleor.io/docs/3.x/developer/extending/apps/synchronous-webhooks) webhooks (and vast Saleor's API). In most cases, creating an App consists of two tasks:
|
||||
|
||||
- Writing webhook's code executing your custom logic.
|
||||
- Developing configuration UI to be displayed in Saleor Dashboard via specialized view (designated in the App's manifest).
|
||||
|
||||
### What's included?
|
||||
|
||||
- 🚀 Communication between Saleor instance and Saleor App
|
||||
- 📖 Manifest with webhooks using custom query
|
||||
|
||||
### Why Next.js
|
||||
|
||||
You can use any preferred technology to create Saleor Apps, but Next.js is among the most efficient for two reasons. The first is the simplicity of maintaining your API endpoints/webhooks and your apps' configuration React front-end in a single, well-organized project. The second reason is the ease and quality of local development and deployment.
|
||||
|
||||
### Learn more about Apps
|
||||
|
||||
[Apps guide](https://docs.saleor.io/docs/3.x/developer/extending/apps/key-concepts)
|
||||
|
||||
[Configuring apps in dashboard](https://docs.saleor.io/docs/3.x/dashboard/apps)
|
||||
|
||||
## Development
|
||||
|
||||
### Requirements
|
||||
|
||||
Before you start, make sure you have installed:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/)
|
||||
- [pnpm](https://pnpm.io/)
|
||||
- [Saleor CLI](https://docs.saleor.io/docs/3.x/cli) - optional, but recommended
|
||||
|
||||
### With CLI
|
||||
|
||||
The easiest way to set up a Saleor app is by using the Saleor CLI.
|
||||
|
||||
[Saleor CLI](https://github.com/saleor/saleor-cli) is designed to save you from the repetitive chores around Saleor development, including creating Apps. It will take the burden of spawning new apps locally, connecting them with Saleor environments, and establishing a tunnel for local development in seconds.
|
||||
|
||||
[Full Saleor CLI reference](https://docs.saleor.io/docs/3.x/developer/cli)
|
||||
|
||||
If you don't have a (free developer) Saleor Cloud account, create one with the following command:
|
||||
|
||||
```
|
||||
saleor register
|
||||
```
|
||||
|
||||
Now you're ready to create your first App:
|
||||
|
||||
```
|
||||
saleor app create [your-app-name]
|
||||
```
|
||||
|
||||
In this step, Saleor CLI will:
|
||||
|
||||
- clone this repository to the specified folder
|
||||
- install dependencies
|
||||
- ask you whether you'd like to install the app in the selected Saleor environment
|
||||
- create `.env` file
|
||||
- start the app in development mode
|
||||
|
||||
Having your app ready, the final thing you want to establish is a tunnel with your Saleor environment. Go to your app's directory first and run:
|
||||
|
||||
```
|
||||
saleor app tunnel
|
||||
```
|
||||
|
||||
Your local application should be available now to the outside world (Saleor instance) for accepting all the events via webhooks.
|
||||
|
||||
A quick note: the next time you come back to your project, it is enough to launch your app in a standard way (and then launch your tunnel as described earlier):
|
||||
|
||||
```
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Without CLI
|
||||
|
||||
1. Install the dependencies by running:
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. Start the local server with:
|
||||
```
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
3. Expose local environment using tunnel:
|
||||
Use tunneling tools like [localtunnel](https://github.com/localtunnel/localtunnel) or [ngrok](https://ngrok.com/).
|
||||
|
||||
4. Install aplication at your dashboard:
|
||||
|
||||
If you use Saleor Cloud or your local server is exposed, you can install your app by following this link:
|
||||
```
|
||||
[YOUR_SALEOR_DASHBOARD_URL]/apps/install?manifestUrl=[YOUR_APP_TUNNEL_MANIFEST_URL]
|
||||
```
|
||||
This template host manifest at `/api/manifest`
|
||||
|
||||
|
||||
You can also install application using GQL or command line. Follow the guide [how to install your app](https://docs.saleor.io/docs/3.x/developer/extending/apps/installing-apps#installation-using-graphql-api) to learn more.
|
||||
|
||||
### Generated schema and typings
|
||||
|
||||
Commands `build` and `dev` would generate schema and typed functions using Saleor's GraphQL endpoint. Commit the `generated` folder to your repo as they are necessary for queries and keeping track of the schema changes.
|
||||
|
||||
[Learn more](https://www.graphql-code-generator.com/) about GraphQL code generation.
|
||||
|
||||
### Storing registration data - APL
|
||||
|
||||
During registration process Saleor API pass the auth token to the app. With this token App can query Saleor API with privileged access (depending on requested permissions during the installation).
|
||||
To store this data, app-template use a different [APL interfaces](https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md).
|
||||
|
||||
The choice of the APL is done using `APL` environment variable. If value is not set, FileAPL is used. Available choices:
|
||||
|
||||
- `file`: no additional setup is required. Good choice for local development. Can't be used for multi tenant-apps or be deployed (not intended for production)
|
||||
- `upstash`: use [Upstash](https://upstash.com/) Redis as storage method. Free account required. Can be used for development and production and supports multi-tenancy. Requires `UPSTASH_URL` and `UPSTASH_TOKEN` environment variables to be set
|
||||
- `vercel`: used by deployments from the Marketplace. It's single-tenant only and only supported by Vercel deployments done with Saleor CLI. Requires `SALEOR_REGISTER_APP_URL` and `SALEOR_DEPLOYMENT_TOKEN` environment variables to be set (handled automatically by the Saleor CLI)
|
||||
|
||||
If you want to use your own database, you can implement your own APL. [Check the documentation to read more.](https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md)
|
72804
apps/data-importer/generated/graphql.ts
Normal file
72804
apps/data-importer/generated/graphql.ts
Normal file
File diff suppressed because it is too large
Load diff
22579
apps/data-importer/generated/schema.graphql
Normal file
22579
apps/data-importer/generated/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
0
apps/data-importer/graphql/fragments/.gitkeep
Normal file
0
apps/data-importer/graphql/fragments/.gitkeep
Normal file
0
apps/data-importer/graphql/mutations/.gitkeep
Normal file
0
apps/data-importer/graphql/mutations/.gitkeep
Normal file
10
apps/data-importer/graphql/mutations/CustomerCreate.graphql
Normal file
10
apps/data-importer/graphql/mutations/CustomerCreate.graphql
Normal file
|
@ -0,0 +1,10 @@
|
|||
mutation CustomerCreate($input: UserCreateInput!) {
|
||||
customerCreate(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
0
apps/data-importer/graphql/queries/.gitkeep
Normal file
0
apps/data-importer/graphql/queries/.gitkeep
Normal file
|
@ -0,0 +1,9 @@
|
|||
query FetchAppDetails {
|
||||
app {
|
||||
id
|
||||
privateMetadata {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
29625
apps/data-importer/graphql/schema.graphql
Normal file
29625
apps/data-importer/graphql/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
0
apps/data-importer/graphql/subscriptions/.gitkeep
Normal file
0
apps/data-importer/graphql/subscriptions/.gitkeep
Normal file
5
apps/data-importer/next-env.d.ts
vendored
Normal file
5
apps/data-importer/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
11
apps/data-importer/next.config.js
Normal file
11
apps/data-importer/next.config.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* TODO This should be solved using transpilePackages option
|
||||
*/
|
||||
const withTM = require("next-transpile-modules")(["nuvo-react"]);
|
||||
|
||||
module.exports = withTM({
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
esmExternals: true,
|
||||
},
|
||||
});
|
67
apps/data-importer/package.json
Normal file
67
apps/data-importer/package.json
Normal file
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "saleor-app-data-importer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
"build": "pnpm generate && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"fetch-schema": "curl https://raw.githubusercontent.com/saleor/saleor/${npm_package_saleor_schemaVersion}/saleor/graphql/schema.graphql > graphql/schema.graphql",
|
||||
"generate": "graphql-codegen",
|
||||
"test": "vitest"
|
||||
},
|
||||
"saleor": {
|
||||
"schemaVersion": "3.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@saleor/app-sdk": "0.26.1",
|
||||
"@saleor/macaw-ui": "^0.7.2",
|
||||
"@urql/exchange-auth": "^1.0.0",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"clsx": "^1.2.1",
|
||||
"dot-object": "^2.1.4",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"jose": "^4.11.2",
|
||||
"jsdom": "^20.0.3",
|
||||
"next": "12.3.4",
|
||||
"nuvo-react": "^1.20.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"urql": "^3.0.3",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"vite": "^4.0.1",
|
||||
"vitest": "^0.25.8",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.13.3",
|
||||
"@graphql-codegen/introspection": "2.2.1",
|
||||
"@graphql-codegen/typed-document-node": "^2.3.3",
|
||||
"@graphql-codegen/typescript": "2.7.3",
|
||||
"@graphql-codegen/typescript-operations": "2.5.3",
|
||||
"@graphql-codegen/typescript-urql": "^3.7.0",
|
||||
"@graphql-codegen/urql-introspection": "2.2.1",
|
||||
"@graphql-typed-document-node/core": "^3.1.1",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/dot-object": "^2.1.2",
|
||||
"@types/node": "^18.8.1",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"eslint": "8.25.0",
|
||||
"eslint-config-next": "12.3.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,tsx}": "eslint --cache --fix",
|
||||
"*.{js,ts,tsx,css,md,json}": "prettier --write"
|
||||
}
|
||||
}
|
7780
apps/data-importer/pnpm-lock.yaml
Normal file
7780
apps/data-importer/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
39
apps/data-importer/saleor-app.ts
Normal file
39
apps/data-importer/saleor-app.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { SaleorApp } from "@saleor/app-sdk/saleor-app";
|
||||
import {APL, FileAPL, SaleorCloudAPL, UpstashAPL, VercelAPL} from "@saleor/app-sdk/APL";
|
||||
|
||||
/**
|
||||
* By default auth data are stored in the `.auth-data.json` (FileAPL).
|
||||
* For multi-tenant applications and deployments please use UpstashAPL.
|
||||
*
|
||||
* To read more about storing auth data, read the
|
||||
* [APL documentation](https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md)
|
||||
*/
|
||||
|
||||
export let apl: APL;
|
||||
switch (process.env.APL) {
|
||||
case "vercel":
|
||||
apl = new VercelAPL();
|
||||
break;
|
||||
case "upstash":
|
||||
// Require `UPSTASH_URL` and `UPSTASH_TOKEN` environment variables
|
||||
apl = new UpstashAPL();
|
||||
break;
|
||||
case "saleor-cloud": {
|
||||
if (!process.env.REST_APL_ENDPOINT || !process.env.REST_APL_TOKEN) {
|
||||
throw new Error("Rest APL is not configured - missing env variables. Check saleor-app.ts");
|
||||
}
|
||||
|
||||
apl = new SaleorCloudAPL({
|
||||
resourceUrl: process.env.REST_APL_ENDPOINT,
|
||||
token: process.env.REST_APL_TOKEN,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
apl = new FileAPL();
|
||||
}
|
||||
|
||||
export const saleorApp = new SaleorApp({
|
||||
apl,
|
||||
});
|
48
apps/data-importer/src/lib/graphql.ts
Normal file
48
apps/data-importer/src/lib/graphql.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { AuthConfig, authExchange } from "@urql/exchange-auth";
|
||||
import {
|
||||
cacheExchange,
|
||||
createClient as urqlCreateClient,
|
||||
dedupExchange,
|
||||
fetchExchange,
|
||||
} from "urql";
|
||||
|
||||
interface IAuthState {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const createClient = (url: string, getAuth: AuthConfig<IAuthState>["getAuth"]) =>
|
||||
urqlCreateClient({
|
||||
url,
|
||||
exchanges: [
|
||||
dedupExchange,
|
||||
cacheExchange,
|
||||
authExchange<IAuthState>({
|
||||
addAuthToOperation: ({ authState, operation }) => {
|
||||
if (!authState || !authState?.token) {
|
||||
return operation;
|
||||
}
|
||||
|
||||
const fetchOptions =
|
||||
typeof operation.context.fetchOptions === "function"
|
||||
? operation.context.fetchOptions()
|
||||
: operation.context.fetchOptions || {};
|
||||
|
||||
return {
|
||||
...operation,
|
||||
context: {
|
||||
...operation.context,
|
||||
fetchOptions: {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...fetchOptions.headers,
|
||||
"Authorization-Bearer": authState.token,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getAuth,
|
||||
}),
|
||||
fetchExchange,
|
||||
],
|
||||
});
|
7
apps/data-importer/src/lib/is-in-iframe.ts
Normal file
7
apps/data-importer/src/lib/is-in-iframe.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export function isInIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
48
apps/data-importer/src/lib/theme-synchronizer.test.tsx
Normal file
48
apps/data-importer/src/lib/theme-synchronizer.test.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { AppBridgeState } from "@saleor/app-sdk/app-bridge";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { ThemeSynchronizer } from "./theme-synchronizer";
|
||||
|
||||
const appBridgeState: AppBridgeState = {
|
||||
ready: true,
|
||||
token: "token",
|
||||
domain: "some-domain.saleor.cloud",
|
||||
theme: "dark",
|
||||
path: "/",
|
||||
locale: "en",
|
||||
id: "app-id",
|
||||
saleorApiUrl: "https://some-domain.saleor.cloud/graphql/",
|
||||
};
|
||||
|
||||
const mockThemeChange = vi.fn();
|
||||
|
||||
vi.mock("@saleor/app-sdk/app-bridge", () => {
|
||||
return {
|
||||
useAppBridge() {
|
||||
return {
|
||||
appBridgeState: appBridgeState,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@saleor/macaw-ui", () => {
|
||||
return {
|
||||
useTheme() {
|
||||
return {
|
||||
setTheme: mockThemeChange,
|
||||
themeType: "light",
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("ThemeSynchronizer", () => {
|
||||
it("Updates MacawUI theme when AppBridgeState theme changes", () => {
|
||||
render(<ThemeSynchronizer />);
|
||||
|
||||
return waitFor(() => {
|
||||
expect(mockThemeChange).toHaveBeenCalledWith("dark");
|
||||
});
|
||||
});
|
||||
});
|
28
apps/data-importer/src/lib/theme-synchronizer.tsx
Normal file
28
apps/data-importer/src/lib/theme-synchronizer.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { useTheme } from "@saleor/macaw-ui";
|
||||
import { memo, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard,
|
||||
* Macaw must be informed about this change from AppBridge.
|
||||
*
|
||||
* If you are not using Macaw, you can remove this.
|
||||
*/
|
||||
function _ThemeSynchronizer() {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
const { setTheme, themeType } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!setTheme || !appBridgeState?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeType !== appBridgeState?.theme) {
|
||||
setTheme(appBridgeState.theme);
|
||||
}
|
||||
}, [appBridgeState?.theme, setTheme, themeType]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ThemeSynchronizer = memo(_ThemeSynchronizer);
|
|
@ -0,0 +1,31 @@
|
|||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as jose from "jose";
|
||||
|
||||
/**
|
||||
* TODO test
|
||||
* TODO extract to app-sdk
|
||||
*/
|
||||
export function useAuthorizedToken(requirePermission: string) {
|
||||
const [authorized, setAuthorized] = useState<boolean | undefined>();
|
||||
|
||||
const { appBridgeState } = useAppBridge();
|
||||
|
||||
useEffect(() => {
|
||||
if (appBridgeState?.token) {
|
||||
const decodedToken = jose.decodeJwt(appBridgeState.token);
|
||||
|
||||
try {
|
||||
const userPermissions = decodedToken.user_permissions as string[];
|
||||
|
||||
if (userPermissions.includes(requirePermission)) {
|
||||
setAuthorized(true);
|
||||
}
|
||||
} catch (e) {
|
||||
setAuthorized(false);
|
||||
}
|
||||
}
|
||||
}, [appBridgeState, requirePermission]);
|
||||
|
||||
return authorized;
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { ColumnAPI } from "nuvo-react";
|
||||
import { z } from "zod";
|
||||
|
||||
const customerColumns: ColumnAPI[] = [
|
||||
{
|
||||
label: "Customer first name",
|
||||
key: "customerCreate.firstName",
|
||||
columnType: "string",
|
||||
},
|
||||
{
|
||||
label: "Customer last name",
|
||||
key: "customerCreate.lastName",
|
||||
columnType: "string",
|
||||
},
|
||||
{
|
||||
label: "Customer email",
|
||||
key: "customerCreate.email",
|
||||
columnType: "email",
|
||||
validations: [
|
||||
{
|
||||
validate: "required",
|
||||
},
|
||||
{
|
||||
validate: "unique",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Note",
|
||||
key: "customerCreate.note",
|
||||
},
|
||||
{
|
||||
label: "External reference",
|
||||
key: "customerCreate.externalReference",
|
||||
description: "ID from another service",
|
||||
},
|
||||
];
|
||||
|
||||
const generateAddressColumns = (labelNamespace: string, keyNamespace: string): ColumnAPI[] => [
|
||||
{
|
||||
label: `${labelNamespace} -> First name`,
|
||||
key: `customerCreate.${keyNamespace}.firstName`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> Last name`,
|
||||
key: `customerCreate.${keyNamespace}.lastName`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> Company name`,
|
||||
key: `customerCreate.${keyNamespace}.companyName`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> Street address 1`,
|
||||
key: `customerCreate.${keyNamespace}.streetAddress1`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> Street address 2`,
|
||||
key: `customerCreate.${keyNamespace}.streetAddress2`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> City`,
|
||||
key: `customerCreate.${keyNamespace}.city`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> City Area`,
|
||||
key: `customerCreate.${keyNamespace}.cityArea`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> Postal code`,
|
||||
key: `customerCreate.${keyNamespace}.postalCode`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> Country`,
|
||||
key: `customerCreate.${keyNamespace}.country`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> Country Area`,
|
||||
key: `customerCreate.${keyNamespace}.countryArea`,
|
||||
},
|
||||
{
|
||||
label: `${labelNamespace} -> Phone`,
|
||||
key: `customerCreate.${keyNamespace}.phone`,
|
||||
},
|
||||
];
|
||||
|
||||
// TODO - enable address columns when mapped
|
||||
const allColumns: ColumnAPI[] = [
|
||||
...customerColumns,
|
||||
// ...generateAddressColumns("Default Billing Address", "defaultBillingAddress"),
|
||||
// ...generateAddressColumns("Default Shipping Address", "defaultShippingAddress"),
|
||||
];
|
||||
|
||||
export const getCustomersModelColumns = () => allColumns;
|
||||
|
||||
const zodAddressSchema = z
|
||||
.object({
|
||||
firstName: z.string().nullish(),
|
||||
lastName: z.string().nullish(),
|
||||
companyName: z.string().nullish(),
|
||||
streetAddress1: z.string().nullish(),
|
||||
streetAddress2: z.string().nullish(),
|
||||
city: z.string().nullish(),
|
||||
cityArea: z.string().nullish(),
|
||||
postalCode: z.string().nullish(),
|
||||
country: z.string().nullish(),
|
||||
countryArea: z.string().nullish(),
|
||||
phone: z.string().nullish(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
export const getResultModelSchema = () =>
|
||||
z.object({
|
||||
customerCreate: z.object({
|
||||
firstName: z.string().nullish(),
|
||||
lastName: z.string().nullish(),
|
||||
email: z.string(),
|
||||
note: z.string().nullish(),
|
||||
externalReference: z.string().nullish(),
|
||||
// defaultBillingAddress: zodAddressSchema,
|
||||
// defaultShippingAddress: zodAddressSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
export type CustomerColumnSchema = z.infer<ReturnType<typeof getResultModelSchema>>;
|
|
@ -0,0 +1,131 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { ConfigureAPI, OnResults, SettingsAPI } from "nuvo-react";
|
||||
import {
|
||||
CustomerColumnSchema,
|
||||
getCustomersModelColumns,
|
||||
getResultModelSchema,
|
||||
} from "./customers-columns-model";
|
||||
import dotObject from "dot-object";
|
||||
import { useAuthorizedToken } from "../../authorization/use-authorized-token";
|
||||
import { Alert, Button } from "@saleor/macaw-ui";
|
||||
import { CustomersImportingResults } from "../customers-results/customers-importing-results";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
import { CloudUpload } from "@material-ui/icons";
|
||||
|
||||
let PassSubmitResult: any;
|
||||
let RejectSubmitResult: any;
|
||||
|
||||
const NuvoImporter = dynamic<ConfigureAPI>(
|
||||
async () => {
|
||||
return import("nuvo-react").then((item) => {
|
||||
PassSubmitResult = item.PassSubmitResult;
|
||||
RejectSubmitResult = item.RejectSubmitResult;
|
||||
return item.NuvoImporter;
|
||||
});
|
||||
},
|
||||
{
|
||||
ssr: false,
|
||||
loading() {
|
||||
return <LinearProgress />;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const columns = getCustomersModelColumns();
|
||||
|
||||
const nuvoSettings: SettingsAPI = {
|
||||
columns,
|
||||
developerMode: true, //todo
|
||||
identifier: "customers",
|
||||
modal: false,
|
||||
style: {
|
||||
buttons: {
|
||||
primary: {
|
||||
background: "black",
|
||||
color: "#fff",
|
||||
},
|
||||
},
|
||||
loader:{
|
||||
loadAnimationColor: '#000'
|
||||
},
|
||||
header: {
|
||||
description: {
|
||||
display: "none",
|
||||
},
|
||||
root: {
|
||||
// display: "none",
|
||||
},
|
||||
},
|
||||
progressBar: {
|
||||
root: {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
dropzone: {
|
||||
icon: {
|
||||
box: {
|
||||
filter: "grayscale(1)",
|
||||
},
|
||||
},
|
||||
root: {
|
||||
background: "#fff",
|
||||
border: "1px dashed #ddd",
|
||||
},
|
||||
},
|
||||
globals: { fontFamily: "Inter", backgroundColor: "transparent" },
|
||||
},
|
||||
title: "Upload customers to Saleor",
|
||||
disableExcelTemplate: true,
|
||||
disableTemplates: true,
|
||||
allowManualInput: true,
|
||||
};
|
||||
|
||||
const licenseKey = process.env.NEXT_PUBLIC_NUVO_LICENSE_KEY as string;
|
||||
|
||||
export const CustomersImporterView = () => {
|
||||
const authorized = useAuthorizedToken("MANAGE_USERS");
|
||||
|
||||
const [importedLines, setImportedLines] = useState<CustomerColumnSchema[] | null>(null);
|
||||
|
||||
const handleResults: OnResults = useCallback((resultArray) => {
|
||||
const parsedResult = resultArray.map((row) =>
|
||||
getResultModelSchema().parse(dotObject.object(row))
|
||||
);
|
||||
|
||||
setImportedLines(parsedResult);
|
||||
}, []);
|
||||
|
||||
if (authorized === undefined) {
|
||||
return <div>Authorizing</div>;
|
||||
}
|
||||
|
||||
if (authorized === false) {
|
||||
return <Alert variant="error">To use this importer you need MANAGER_USERS permission</Alert>;
|
||||
}
|
||||
|
||||
if (importedLines) {
|
||||
return <CustomersImportingResults importedLines={importedLines} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NuvoImporter
|
||||
renderUploadButton={({ launch }) => {
|
||||
return (
|
||||
<Button
|
||||
size="large"
|
||||
startIcon={<CloudUpload />}
|
||||
variant="primary"
|
||||
color="primary"
|
||||
onClick={launch}
|
||||
>
|
||||
Upload file
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
onResults={handleResults}
|
||||
licenseKey={licenseKey}
|
||||
settings={nuvoSettings}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,103 @@
|
|||
import { Box, TableCell, TableRow, Typography } from "@material-ui/core";
|
||||
import { Done, Error, HourglassEmpty } from "@material-ui/icons";
|
||||
import React, { memo, useCallback, useEffect, useState } from "react";
|
||||
import { useCustomerCreateMutation } from "../../../../generated/graphql";
|
||||
import { CustomerColumnSchema } from "../customers-importer-nuvo/customers-columns-model";
|
||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { Button } from "@saleor/macaw-ui";
|
||||
|
||||
type Props = {
|
||||
importedModel: CustomerColumnSchema;
|
||||
doImport: boolean;
|
||||
};
|
||||
|
||||
const ImportedStatus = ({ id }: { id: string }) => {
|
||||
const { appBridge } = useAppBridge();
|
||||
|
||||
return (
|
||||
<Box style={{ gap: 20, display: "flex", alignItems: "center" }}>
|
||||
<Done color="primary" width={30} />
|
||||
<span
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
// newContext: true, // open in new context but dashboard has a bug here
|
||||
to: `/customers/${id}`,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
Imported with ID <code>{id}</code>
|
||||
</span>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorStatus = ({ message, onRetry }: { message: string; onRetry(): void }) => {
|
||||
return (
|
||||
<Box style={{ gap: 20, display: "flex", alignItems: "center" }}>
|
||||
<Error width={30} color="error" />
|
||||
<span color="error">Error importing: {message}</span>
|
||||
<Button onClick={onRetry}>Retry</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
const PendingStatus = () => (
|
||||
<Box style={{ gap: 20, display: "flex", alignItems: "center" }}>
|
||||
<HourglassEmpty width={30} />
|
||||
<span>Importing...</span>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const CustomerImportingRow = (props: Props) => {
|
||||
const [mutationResult, mutate] = useCustomerCreateMutation();
|
||||
const triggerMutation = useCallback(() => {
|
||||
mutate({
|
||||
input: {
|
||||
...props.importedModel.customerCreate,
|
||||
// todo map address
|
||||
defaultShippingAddress: null,
|
||||
defaultBillingAddress: null,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
}, [props.importedModel, mutate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
props.doImport &&
|
||||
!mutationResult.data &&
|
||||
!mutationResult.error &&
|
||||
!mutationResult.fetching
|
||||
) {
|
||||
triggerMutation();
|
||||
}
|
||||
}, [props.doImport, mutate, mutationResult, triggerMutation]);
|
||||
|
||||
const renderStatus = () => {
|
||||
if (mutationResult.data?.customerCreate?.user?.id) {
|
||||
return <ImportedStatus id={mutationResult.data?.customerCreate?.user?.id} />;
|
||||
}
|
||||
|
||||
if (mutationResult.data?.customerCreate?.errors) {
|
||||
return (
|
||||
<ErrorStatus
|
||||
onRetry={triggerMutation}
|
||||
message={mutationResult.data?.customerCreate?.errors[0].message ?? "Error importing"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (mutationResult.fetching) {
|
||||
return <PendingStatus />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>{props.importedModel.customerCreate.email}</TableCell>
|
||||
<TableCell>{renderStatus()}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
import { Button } from "@saleor/macaw-ui";
|
||||
import { Table, TableBody, Typography } from "@material-ui/core";
|
||||
import { CustomerImportingRow } from "./customer-importing-row";
|
||||
import React, { useState } from "react";
|
||||
import { CustomerColumnSchema } from "../customers-importer-nuvo/customers-columns-model";
|
||||
|
||||
export const CustomersImportingResults = ({
|
||||
importedLines,
|
||||
}: {
|
||||
importedLines: CustomerColumnSchema[];
|
||||
}) => {
|
||||
const [importingStarted, setImportingStarted] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography paragraph variant="h3">Customers rows from imported file</Typography>
|
||||
|
||||
<Typography paragraph>
|
||||
Lines will be imported one by one. Failed imports can be retried, but performed operations
|
||||
must be reverted manually. Users will be set to inactive.
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
Customers will <strong>not</strong> be informed or notified by this operation.
|
||||
</Typography>
|
||||
|
||||
{!importingStarted && (
|
||||
<Button
|
||||
style={{ margin: "20px 0" }}
|
||||
variant="primary"
|
||||
onClick={() => setImportingStarted(true)}
|
||||
>
|
||||
Start importing
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Table style={{marginTop: 50}}>
|
||||
<TableBody>
|
||||
{importedLines.map((row) => (
|
||||
<CustomerImportingRow
|
||||
doImport={importingStarted}
|
||||
key={row.customerCreate.email}
|
||||
importedModel={row}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
28
apps/data-importer/src/modules/ui/app-icon.tsx
Normal file
28
apps/data-importer/src/modules/ui/app-icon.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
appIconContainer: {
|
||||
background: "rgb(58, 86, 199)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRadius: "50%",
|
||||
color: "#fff",
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
});
|
||||
|
||||
export function AppIcon() {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.appIconContainer}>
|
||||
<div>
|
||||
<Typography variant="h2">DI</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
68
apps/data-importer/src/modules/ui/main-bar.tsx
Normal file
68
apps/data-importer/src/modules/ui/main-bar.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { Paper, PaperProps } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
height: 96,
|
||||
padding: "0 32px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: `1px solid ${theme.palette.grey.A100} `,
|
||||
},
|
||||
leftColumn: {
|
||||
marginRight: "auto",
|
||||
},
|
||||
rightColumn: {},
|
||||
iconColumn: {
|
||||
marginRight: 24,
|
||||
},
|
||||
appName: { fontSize: 24, margin: 0 },
|
||||
appAuthor: {
|
||||
fontSize: 12,
|
||||
textTransform: "uppercase",
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 500,
|
||||
margin: 0,
|
||||
},
|
||||
bottomMargin: {
|
||||
marginBottom: 32,
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
author: string;
|
||||
rightColumnContent?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
bottomMargin?: boolean;
|
||||
} & PaperProps;
|
||||
|
||||
export function MainBar({
|
||||
name,
|
||||
author,
|
||||
rightColumnContent,
|
||||
className,
|
||||
icon,
|
||||
bottomMargin,
|
||||
}: Props) {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
className={clsx(styles.root, className, {
|
||||
[styles.bottomMargin]: bottomMargin,
|
||||
})}
|
||||
>
|
||||
{icon && <div className={styles.iconColumn}>{icon}</div>}
|
||||
<div className={styles.leftColumn}>
|
||||
<h1 className={styles.appName}>{name}</h1>
|
||||
<h1 className={styles.appAuthor}>{author}</h1>
|
||||
</div>
|
||||
<div className={styles.rightColumn}>{rightColumnContent}</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
8
apps/data-importer/src/no-ssr-wrapper.tsx
Normal file
8
apps/data-importer/src/no-ssr-wrapper.tsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
import React, { PropsWithChildren } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const Wrapper = (props: PropsWithChildren<{}>) => <React.Fragment>{props.children}</React.Fragment>;
|
||||
|
||||
export const NoSSRWrapper = dynamic(() => Promise.resolve(Wrapper), {
|
||||
ssr: false,
|
||||
});
|
57
apps/data-importer/src/pages/_app.tsx
Normal file
57
apps/data-importer/src/pages/_app.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import "../styles/globals.css";
|
||||
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
||||
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
||||
import { ThemeProvider as MacawUIThemeProvider } from "@saleor/macaw-ui";
|
||||
import React, { PropsWithChildren, useEffect } from "react";
|
||||
import { AppProps } from "next/app";
|
||||
|
||||
import GraphQLProvider from "../providers/GraphQLProvider";
|
||||
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
||||
import { NoSSRWrapper } from "../no-ssr-wrapper";
|
||||
|
||||
const themeOverrides: Partial<Theme> = {
|
||||
/**
|
||||
* You can override MacawUI theme here
|
||||
*/
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensure instance is a singleton.
|
||||
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
|
||||
*/
|
||||
const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
||||
|
||||
/**
|
||||
* That's a hack required by Macaw-UI incompatibility with React@18
|
||||
*/
|
||||
const ThemeProvider = MacawUIThemeProvider as React.FC<
|
||||
PropsWithChildren<{ overrides?: Partial<Theme>; ssr: boolean }>
|
||||
>;
|
||||
|
||||
function NextApp({ Component, pageProps }: AppProps) {
|
||||
/**
|
||||
* Configure JSS (used by MacawUI) for SSR. If Macaw is not used, can be removed.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const jssStyles = document.querySelector("#jss-server-side");
|
||||
if (jssStyles) {
|
||||
jssStyles?.parentElement?.removeChild(jssStyles);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NoSSRWrapper>
|
||||
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
||||
<ThemeProvider overrides={themeOverrides} ssr>
|
||||
<ThemeSynchronizer />
|
||||
<RoutePropagator />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</AppBridgeProvider>
|
||||
</NoSSRWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default NextApp;
|
32
apps/data-importer/src/pages/api/manifest.ts
Normal file
32
apps/data-importer/src/pages/api/manifest.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { AppManifest } from "@saleor/app-sdk/types";
|
||||
|
||||
import packageJson from "../../../package.json";
|
||||
|
||||
export default createManifestHandler({
|
||||
async manifestFactory(context) {
|
||||
const manifest: AppManifest = {
|
||||
name: packageJson.name,
|
||||
tokenTargetUrl: `${context.appBaseUrl}/api/register`,
|
||||
appUrl: context.appBaseUrl,
|
||||
permissions: ["MANAGE_USERS"],
|
||||
id: "saleor.app",
|
||||
version: packageJson.version,
|
||||
webhooks: [
|
||||
/**
|
||||
* Configure webhooks here. They will be created in Saleor during installation
|
||||
* Read more
|
||||
* https://docs.saleor.io/docs/3.x/developer/api-reference/objects/webhook
|
||||
*/
|
||||
],
|
||||
extensions: [
|
||||
/**
|
||||
* Optionally, extend Dashboard with custom UIs
|
||||
* https://docs.saleor.io/docs/3.x/developer/extending/apps/extending-dashboard-with-apps
|
||||
*/
|
||||
],
|
||||
};
|
||||
|
||||
return manifest;
|
||||
},
|
||||
});
|
9
apps/data-importer/src/pages/api/register.ts
Normal file
9
apps/data-importer/src/pages/api/register.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
|
||||
|
||||
import { saleorApp } from "../../../saleor-app";
|
||||
|
||||
/**
|
||||
* Required endpoint, called by Saleor to install app.
|
||||
* It will exchange tokens with app, so saleorApp.apl will contain token
|
||||
*/
|
||||
export default createAppRegisterHandler(saleorApp);
|
86
apps/data-importer/src/pages/importer.tsx
Normal file
86
apps/data-importer/src/pages/importer.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { NextPage } from "next";
|
||||
import React, { ComponentProps } from "react";
|
||||
import { Container, Divider, Typography } from "@material-ui/core";
|
||||
import { Button, makeStyles, PageTab, PageTabs, SaleorTheme } from "@saleor/macaw-ui";
|
||||
import { CustomersImporterView } from "../modules/customers/customers-importer-nuvo/customers-importer-view";
|
||||
import GraphQLProvider from "../providers/GraphQLProvider";
|
||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { MainBar } from "../modules/ui/main-bar";
|
||||
import { AppIcon } from "../modules/ui/app-icon";
|
||||
|
||||
type Tab = "customers";
|
||||
|
||||
const useStyles = makeStyles((theme: SaleorTheme) => ({
|
||||
wrapper: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
minHeight: `calc(100vh - 100px)`,
|
||||
},
|
||||
}));
|
||||
|
||||
const ImporterPage: NextPage = () => {
|
||||
const [activeTab, setActiveTab] = React.useState<Tab>("customers");
|
||||
const styles = useStyles();
|
||||
|
||||
const { appBridge } = useAppBridge();
|
||||
|
||||
const openInNewTab = (url: string) => {
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
to: url,
|
||||
newContext: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<MainBar
|
||||
bottomMargin
|
||||
icon={<AppIcon />}
|
||||
name="Data Importer"
|
||||
author="By Saleor Commerce"
|
||||
rightColumnContent={
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
openInNewTab("https://github.com/saleor/saleor-app-data-importer");
|
||||
}}
|
||||
variant="tertiary"
|
||||
>
|
||||
Repository
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
openInNewTab("https://github.com/saleor/apps/discussions");
|
||||
}}
|
||||
variant="tertiary"
|
||||
>
|
||||
Request a feature
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Container>
|
||||
<PageTabs
|
||||
style={{ marginBottom: 20, marginTop: 20 }}
|
||||
value={activeTab}
|
||||
onChange={(e) => setActiveTab(e as Tab)}
|
||||
>
|
||||
<PageTab value="customers" label="Customers" />
|
||||
<PageTab disabled value="orders" label="Orders (coming soon)" />
|
||||
<PageTab disabled value="products" label="Products (coming soon)" />
|
||||
</PageTabs>
|
||||
<Divider style={{ marginBottom: 50 }} />
|
||||
{activeTab === "customers" && <CustomersImporterView />}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WrappedPage = (props: ComponentProps<NextPage>) => (
|
||||
<GraphQLProvider>
|
||||
<ImporterPage {...props} />
|
||||
</GraphQLProvider>
|
||||
);
|
||||
|
||||
export default WrappedPage;
|
33
apps/data-importer/src/pages/index.tsx
Normal file
33
apps/data-importer/src/pages/index.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { NextPage } from "next";
|
||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { useEffect } from "react";
|
||||
import { useIsMounted } from "usehooks-ts";
|
||||
import { useRouter } from "next/router";
|
||||
import { isInIframe } from "../lib/is-in-iframe";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
|
||||
const IndexPage: NextPage = () => {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
const isMounted = useIsMounted();
|
||||
const { replace } = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isMounted() && appBridgeState?.ready) {
|
||||
replace("/importer");
|
||||
}
|
||||
}, [isMounted, appBridgeState?.ready, replace]);
|
||||
|
||||
if (isInIframe()) {
|
||||
return <LinearProgress />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Saleor Data Importer</h1>
|
||||
<p>This is Saleor App that allows importing data from CSV</p>
|
||||
<p>Install app in your Saleor instance and open in with Dashboard</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexPage;
|
17
apps/data-importer/src/providers/GraphQLProvider.tsx
Normal file
17
apps/data-importer/src/providers/GraphQLProvider.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Provider } from "urql";
|
||||
|
||||
import { createClient } from "../lib/graphql";
|
||||
|
||||
function GraphQLProvider(props: PropsWithChildren<{}>) {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
|
||||
const client = createClient(appBridgeState?.saleorApiUrl!, async () =>
|
||||
Promise.resolve({ token: appBridgeState?.token! })
|
||||
);
|
||||
|
||||
return <Provider value={client} {...props} />;
|
||||
}
|
||||
|
||||
export default GraphQLProvider;
|
BIN
apps/data-importer/src/public/favicon.ico
Normal file
BIN
apps/data-importer/src/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
4
apps/data-importer/src/public/vercel.svg
Normal file
4
apps/data-importer/src/public/vercel.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
6
apps/data-importer/src/setup-tests.ts
Normal file
6
apps/data-importer/src/setup-tests.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Add test setup logic here
|
||||
*
|
||||
* https://vitest.dev/config/#setupfiles
|
||||
*/
|
||||
export {}
|
17
apps/data-importer/src/styles/globals.css
Normal file
17
apps/data-importer/src/styles/globals.css
Normal file
|
@ -0,0 +1,17 @@
|
|||
body {
|
||||
font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
color: #111;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f6f8fa;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
padding: 0.75rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
20
apps/data-importer/tsconfig.json
Normal file
20
apps/data-importer/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
13
apps/data-importer/vitest.config.ts
Normal file
13
apps/data-importer/vitest.config.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
passWithNoTests: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "./src/setup-tests.ts",
|
||||
css: false,
|
||||
},
|
||||
});
|
Loading…
Reference in a new issue