Apps marketplace (#2850)
* Add new Apps List * Update apps routing * Add marketplace apps list * Update apps urls * Update app list style * Add installed apps section * Update apps sections and connect actions with mutations * Add latest missing buttons and labels to installed apps list * Update installed apps list * Update installed apps list * Add tests and marketplace error handling * Update environment configuration * Update GitHub actions env configuration * Refactor AppListCard component * Test InstallWithManifestFormButton * Test AppListCard * Extract InstalledAppListRow with tests * Update GitHub actions env configuration * Tests of apps dialogs * Update GitHub actions env configuration * Update messages * Update GitHub actions env configuration * Quote untrusted GitHub actions variables * Change useFetch to useMarketplaceApps and add tests * Fix strict null check errors * Refactor apps details components * Add strict null checks for /new-apps/ components
This commit is contained in:
parent
ee19e42e7e
commit
1621feb691
112 changed files with 3907 additions and 114 deletions
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -26,9 +26,11 @@ Modify API_URI if you want test instance to use custom backend. CYPRESS_API_URI
|
|||
|
||||
API_URI=https://automation-dashboard.staging.saleor.cloud/graphql/
|
||||
MARKETPLACE_URL=https://apps.saleor.io
|
||||
APPS_MARKETPLACE_API_URI=https://marketplace-gray.vercel.app/api/v2/saleor-apps
|
||||
SALEOR_APPS_ENDPOINT=https://apps.saleor.io/api/saleor-apps
|
||||
|
||||
### Do you want to run more stable tests?
|
||||
|
||||
To run all tests, just select the stable checkbox. To speed up tests, increase the number of containers. Tests will be re-run only when the "run e2e" label is added.
|
||||
|
||||
1. [ ] stable
|
||||
|
|
1
.github/workflows/deploy-cloud.yaml
vendored
1
.github/workflows/deploy-cloud.yaml
vendored
|
@ -23,6 +23,7 @@ jobs:
|
|||
# SALEOR_APPS_PAGE_PATH=saleor-apps
|
||||
# SALEOR_APPS_JSON_PATH=api/saleor-apps
|
||||
# APP_TEMPLATE_GALLERY_PATH=template-gallery
|
||||
# APPS_MARKETPLACE_API_URI: "https://apps.saleor.io/api/v2/saleor-apps"
|
||||
IS_CLOUD_INSTANCE: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
1
.github/workflows/deploy-demo-staging.yaml
vendored
1
.github/workflows/deploy-demo-staging.yaml
vendored
|
@ -26,6 +26,7 @@ jobs:
|
|||
# SALEOR_APPS_PAGE_PATH=saleor-apps
|
||||
# SALEOR_APPS_JSON_PATH=api/saleor-apps
|
||||
# APP_TEMPLATE_GALLERY_PATH=template-gallery
|
||||
# APPS_MARKETPLACE_API_URI: "https://apps.saleor.io/api/v2/saleor-apps"
|
||||
ENVIRONMENT: demo-staging
|
||||
DEMO_MODE: true
|
||||
steps:
|
||||
|
|
1
.github/workflows/deploy-demo.yaml
vendored
1
.github/workflows/deploy-demo.yaml
vendored
|
@ -21,6 +21,7 @@ jobs:
|
|||
# SALEOR_APPS_PAGE_PATH=saleor-apps
|
||||
# SALEOR_APPS_JSON_PATH=api/saleor-apps
|
||||
# APP_TEMPLATE_GALLERY_PATH=template-gallery
|
||||
# APPS_MARKETPLACE_API_URI: "https://apps.saleor.io/api/v2/saleor-apps"
|
||||
ENVIRONMENT: demo
|
||||
DEMO_MODE: true
|
||||
steps:
|
||||
|
|
1
.github/workflows/deploy-master-staging.yaml
vendored
1
.github/workflows/deploy-master-staging.yaml
vendored
|
@ -21,6 +21,7 @@ jobs:
|
|||
SALEOR_APPS_PAGE_PATH: saleor-apps
|
||||
SALEOR_APPS_JSON_PATH: api/saleor-apps
|
||||
APP_TEMPLATE_GALLERY_PATH: template-gallery
|
||||
APPS_MARKETPLACE_API_URI: "https://marketplace-gray.vercel.app/api/v2/saleor-apps"
|
||||
ENVIRONMENT: saleor-master-staging
|
||||
IS_CLOUD_INSTANCE: true
|
||||
steps:
|
||||
|
|
1
.github/workflows/deploy-staging.yaml
vendored
1
.github/workflows/deploy-staging.yaml
vendored
|
@ -28,6 +28,7 @@ jobs:
|
|||
# SALEOR_APPS_PAGE_PATH=saleor-apps
|
||||
# SALEOR_APPS_JSON_PATH=api/saleor-apps
|
||||
# APP_TEMPLATE_GALLERY_PATH=template-gallery
|
||||
# APPS_MARKETPLACE_API_URI: "https://marketplace-gray.vercel.app/api/v2/saleor-apps"
|
||||
VERSION: ${{ github.event.inputs.git_ref || github.ref_name }}
|
||||
IS_CLOUD_INSTANCE: true
|
||||
steps:
|
||||
|
|
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
|
@ -108,7 +108,7 @@ jobs:
|
|||
pattern: (http|https)://[a-zA-Z0-9.-]+/graphql/?
|
||||
fallback_uri: ${{ secrets.CYPRESS_API_URI }}
|
||||
run: |
|
||||
echo "::set-output name=custom_api_uri::$(echo $pull_request_body | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1 | { read custom_uri; if [ -z "$custom_uri" ]; then echo "$fallback_uri"; else echo "$custom_uri"; fi })"
|
||||
echo "::set-output name=custom_api_uri::$(echo "$pull_request_body" | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1 | { read custom_uri; if [ -z "$custom_uri" ]; then echo "$fallback_uri"; else echo "$custom_uri"; fi })"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
|
16
.github/workflows/test-env-deploy.yml
vendored
16
.github/workflows/test-env-deploy.yml
vendored
|
@ -58,7 +58,7 @@ jobs:
|
|||
prefix: API_URI=
|
||||
pattern: (http|https)://[a-zA-Z0-9.-]+/graphql/?
|
||||
run: |
|
||||
echo "::set-output name=custom_api_uri::$(echo $pull_request_body | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)"
|
||||
echo "::set-output name=custom_api_uri::$(echo "$pull_request_body" | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)"
|
||||
- name: Get MARKETPLACE_URL
|
||||
id: marketplace_url
|
||||
# Search for MARKETPLACE_URL in PR description
|
||||
|
@ -67,12 +67,22 @@ jobs:
|
|||
prefix: MARKETPLACE_URL=
|
||||
pattern: (http|https)://[a-zA-Z0-9.-]+/?
|
||||
run: |
|
||||
echo "::set-output name=custom_marketplace_url::$(echo $pull_request_body | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)"
|
||||
echo "::set-output name=custom_marketplace_url::$(echo "$pull_request_body" | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)"
|
||||
- name: Get APPS_MARKETPLACE_API_URI
|
||||
id: apps_marketplace_api_uri
|
||||
# Search for APPS_MARKETPLACE_API_URI in PR description
|
||||
env:
|
||||
pull_request_body: ${{ github.event.pull_request.body }}
|
||||
prefix: APPS_MARKETPLACE_API_URI=
|
||||
pattern: (http|https)://[a-zA-Z0-9.-]+[a-zA-Z0-9/-]+/?
|
||||
run: |
|
||||
echo "::set-output name=custom_apps_marketplace_api_uri::$(echo "$pull_request_body" | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)"
|
||||
- name: Run build
|
||||
env:
|
||||
# Use custom API_URI or the default one
|
||||
API_URI: ${{ steps.api_uri.outputs.custom_api_uri || 'https://qa.staging.saleor.cloud/graphql/' }}
|
||||
MARKETPLACE_URL: ${{ steps.marketplace_url.outputs.custom_marketplace_url }}
|
||||
APPS_MARKETPLACE_API_URI: ${{ steps.apps_marketplace_api_uri.outputs.custom_apps_marketplace_api_uri }}
|
||||
APP_MOUNT_URI: /
|
||||
STATIC_URL: /
|
||||
IS_CLOUD_INSTANCE: true
|
||||
|
@ -193,7 +203,7 @@ jobs:
|
|||
pattern: (http|https)://[a-zA-Z0-9.-]+/graphql/?
|
||||
fallback_uri: ${{ secrets.CYPRESS_API_URI }}
|
||||
run: |
|
||||
echo "::set-output name=custom_api_uri::$(echo $pull_request_body | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1 | { read custom_uri; if [ -z "$custom_uri" ]; then echo "$fallback_uri"; else echo "$custom_uri"; fi })"
|
||||
echo "::set-output name=custom_api_uri::$(echo "$pull_request_body" | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1 | { read custom_uri; if [ -z "$custom_uri" ]; then echo "$fallback_uri"; else echo "$custom_uri"; fi })"
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
|
|
@ -21,6 +21,8 @@ COPY src/ src/
|
|||
ARG API_URI
|
||||
ARG APP_MOUNT_URI
|
||||
ARG MARKETPLACE_URL
|
||||
ARG APPS_MARKETPLACE_API_URI,
|
||||
ARG APPS_TUNNEL_URL_KEYWORDS,
|
||||
ARG SALEOR_APPS_ENDPOINT
|
||||
ARG STATIC_URL
|
||||
ARG SKIP_SOURCEMAPS
|
||||
|
@ -28,6 +30,8 @@ ARG SKIP_SOURCEMAPS
|
|||
ENV API_URI ${API_URI:-http://localhost:8000/graphql/}
|
||||
ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/dashboard/}
|
||||
ENV MARKETPLACE_URL ${MARKETPLACE_URL}
|
||||
ENV APPS_MARKETPLACE_API_URI ${APPS_MARKETPLACE_API_URI}
|
||||
ENV APPS_TUNNEL_URL_KEYWORDS ${APPS_TUNNEL_URL_KEYWORDS}
|
||||
ENV SALEOR_APPS_ENDPOINT=${SALEOR_APPS_ENDPOINT}
|
||||
ENV STATIC_URL ${STATIC_URL:-/dashboard/}
|
||||
ENV SKIP_SOURCEMAPS ${SKIP_SOURCEMAPS:-true}
|
||||
|
|
|
@ -12,10 +12,14 @@ Create `.env` file in a root directory or set environment variables with the fol
|
|||
- `STATIC_URL` - URL where the static files are located.
|
||||
E.g., if you use an S3 bucket, you should set it to the bucket's URL. By default, Saleor assumes you serve static files from the root of your site at "http://localhost:9000/".
|
||||
|
||||
- `MARKETPLACE_URL` - URL where Marketplace App is located. If not found, it will not render a navigation link to the Marketplace.
|
||||
- `MARKETPLACE_URL` - URL where Marketplace App is located. If not found, it will not render a navigation link to the Marketplace (deprecated in favor of `APPS_MARKETPLACE_API_URI`).
|
||||
|
||||
- `SALEOR_APPS_PAGE_PATH` - Path appended to `MARKETPLACE_URL` to render Saleor Apps page.
|
||||
- `SALEOR_APPS_PAGE_PATH` - Path appended to `MARKETPLACE_URL` to render Saleor Apps page (deprecated).
|
||||
|
||||
- `SALEOR_APPS_JSON_PATH` - Path appended to `MARKETPLACE_URL` to fetch a list of Saleor Apps as JSON.
|
||||
- `SALEOR_APPS_JSON_PATH` - Path appended to `MARKETPLACE_URL` to fetch a list of Saleor Apps as JSON (deprecated in favor of `APPS_MARKETPLACE_API_URI`).
|
||||
|
||||
- `APP_TEMPLATE_GALLERY_PATH` - Path appended to `MARKETPLACE_URL` to render App Template Gallery page.
|
||||
- `APP_TEMPLATE_GALLERY_PATH` - Path appended to `MARKETPLACE_URL` to render App Template Gallery page (deprecated).
|
||||
|
||||
- `APPS_MARKETPLACE_API_URI` - URI of Marketplace API to fetch list of Apps in JSON.
|
||||
|
||||
- `APPS_TUNNEL_URL_KEYWORDS` - Custom apps tunnel URL keywords.
|
||||
|
|
|
@ -70,6 +70,10 @@
|
|||
"+a+2ug": {
|
||||
"string": "Members"
|
||||
},
|
||||
"+b3KCV": {
|
||||
"context": "button",
|
||||
"string": "Activate"
|
||||
},
|
||||
"+c/f61": {
|
||||
"context": "retry installation",
|
||||
"string": "Retry"
|
||||
|
@ -200,6 +204,10 @@
|
|||
"context": "section header",
|
||||
"string": "Media"
|
||||
},
|
||||
"/PhkXi": {
|
||||
"context": "section header",
|
||||
"string": "Installed Apps"
|
||||
},
|
||||
"/TF6BZ": {
|
||||
"string": "Search Products"
|
||||
},
|
||||
|
@ -233,6 +241,10 @@
|
|||
"context": "products in collection",
|
||||
"string": "Products in {name}"
|
||||
},
|
||||
"/eTYOp": {
|
||||
"context": "app support",
|
||||
"string": "Support"
|
||||
},
|
||||
"/glQgs": {
|
||||
"string": "No channels found"
|
||||
},
|
||||
|
@ -378,6 +390,10 @@
|
|||
"context": "selected customer channel subtitle",
|
||||
"string": "Customer will be sent the gift card code via this channels email address"
|
||||
},
|
||||
"0vsMRq": {
|
||||
"context": "delete custom app",
|
||||
"string": "Deleting this app, you will delete all the data and webhooks regarding this app."
|
||||
},
|
||||
"1+M/52": {
|
||||
"context": "section header",
|
||||
"string": "Webhook Status"
|
||||
|
@ -617,6 +633,10 @@
|
|||
"context": "button",
|
||||
"string": "Enable Dark Mode"
|
||||
},
|
||||
"2rJcFa": {
|
||||
"context": "delete app",
|
||||
"string": "Deleting this app, you will remove installation of the app. If you are paying for app subscription, remember to unsubscribe from the app in Saleor Marketplace."
|
||||
},
|
||||
"2yU+q9": {
|
||||
"context": "tab name",
|
||||
"string": "All Warehouses"
|
||||
|
@ -784,6 +804,10 @@
|
|||
"context": "order status",
|
||||
"string": "Returned"
|
||||
},
|
||||
"4IgzXs": {
|
||||
"context": "label",
|
||||
"string": "App Manifest URL"
|
||||
},
|
||||
"4JW9iJ": {
|
||||
"context": "home section name",
|
||||
"string": "Home"
|
||||
|
@ -1326,6 +1350,10 @@
|
|||
"context": "button",
|
||||
"string": "Back to homepage"
|
||||
},
|
||||
"94oZR0": {
|
||||
"context": "deactivate app billing info",
|
||||
"string": "You will be still billed for the app."
|
||||
},
|
||||
"95oJ5d": {
|
||||
"context": "button",
|
||||
"string": "Go back to dashboard"
|
||||
|
@ -1500,6 +1528,10 @@
|
|||
"context": "order discount was updated automatically event title",
|
||||
"string": "Order discount was updated automatically updated"
|
||||
},
|
||||
"ARfw53": {
|
||||
"context": "description",
|
||||
"string": "Install apps created by Saleor team or install external apps."
|
||||
},
|
||||
"AVF5T5": {
|
||||
"context": "voucher end date, switch button",
|
||||
"string": "Set end date"
|
||||
|
@ -1849,6 +1881,9 @@
|
|||
"context": "order history message",
|
||||
"string": "Order was refunded by {refundedBy}"
|
||||
},
|
||||
"D3idYv": {
|
||||
"string": "Settings"
|
||||
},
|
||||
"D4CsYK": {
|
||||
"context": "status filter label",
|
||||
"string": "Status"
|
||||
|
@ -2088,6 +2123,10 @@
|
|||
"context": "dialog header",
|
||||
"string": "Select a page type"
|
||||
},
|
||||
"F2cR19": {
|
||||
"context": "error message",
|
||||
"string": "Error details: {message}"
|
||||
},
|
||||
"F3Upht": {
|
||||
"string": "Product type deleted"
|
||||
},
|
||||
|
@ -2290,6 +2329,10 @@
|
|||
"context": "order history message",
|
||||
"string": "Order was placed"
|
||||
},
|
||||
"GKQGKG": {
|
||||
"context": "app data privacy",
|
||||
"string": "Data privacy"
|
||||
},
|
||||
"GLX9II": {
|
||||
"context": "billing address",
|
||||
"string": "Same as shipping address"
|
||||
|
@ -2874,6 +2917,10 @@
|
|||
"context": "dialog title",
|
||||
"string": "Delete products"
|
||||
},
|
||||
"KDvRBi": {
|
||||
"context": "button",
|
||||
"string": "Install"
|
||||
},
|
||||
"KFv8hX": {
|
||||
"string": "An attribute already exists."
|
||||
},
|
||||
|
@ -3117,6 +3164,10 @@
|
|||
"MYA6EV": {
|
||||
"string": "Third party apps are installed with App Manifests. They contain UI accessible from dashboard and can extend it."
|
||||
},
|
||||
"MaTR88": {
|
||||
"context": "header",
|
||||
"string": "Integrations"
|
||||
},
|
||||
"MbZHXE": {
|
||||
"context": "column title balance",
|
||||
"string": "Balance"
|
||||
|
@ -3325,6 +3376,10 @@
|
|||
"O95R3Z": {
|
||||
"string": "Phone"
|
||||
},
|
||||
"O987DI": {
|
||||
"context": "deactivate named app",
|
||||
"string": "Are you sure you want to disable {name}? Your data will be kept until you reactivate the app."
|
||||
},
|
||||
"O9QPe1": {
|
||||
"context": "dialog content",
|
||||
"string": "{counter,plural,one{Are you sure you want to delete this voucher?} other{Are you sure you want to delete {displayQuantity} vouchers?}}"
|
||||
|
@ -3804,6 +3859,10 @@
|
|||
"Rj8LxK": {
|
||||
"string": "Add search engine title and description to make this collection easier to find"
|
||||
},
|
||||
"RjT+Vo": {
|
||||
"context": "label",
|
||||
"string": "Tunnel - development"
|
||||
},
|
||||
"Rjs1CD": {
|
||||
"context": "button",
|
||||
"string": "Continue"
|
||||
|
@ -4330,6 +4389,10 @@
|
|||
"context": "section header",
|
||||
"string": "Staff Member Information"
|
||||
},
|
||||
"VTq2gn": {
|
||||
"context": "section header",
|
||||
"string": "Coming Soon"
|
||||
},
|
||||
"VZsE96": {
|
||||
"string": "Collection Name"
|
||||
},
|
||||
|
@ -4671,6 +4734,10 @@
|
|||
"Xtd0AT": {
|
||||
"string": "Original String"
|
||||
},
|
||||
"Xu4ech": {
|
||||
"context": "deactivate app",
|
||||
"string": "Are you sure you want to disable this app? Your data will be kept until you reactivate the app."
|
||||
},
|
||||
"XwQQ1f": {
|
||||
"context": "checkbox label description",
|
||||
"string": "All fulfillments will be automatically approved"
|
||||
|
@ -5252,6 +5319,10 @@
|
|||
"context": "input label",
|
||||
"string": "New Password"
|
||||
},
|
||||
"cMov2n": {
|
||||
"context": "error message",
|
||||
"string": "Error loading apps marketplace"
|
||||
},
|
||||
"cNSLLO": {
|
||||
"context": "button",
|
||||
"string": "Unassign and save"
|
||||
|
@ -5509,6 +5580,10 @@
|
|||
"context": "attributes, section header",
|
||||
"string": "Variant Attributes"
|
||||
},
|
||||
"f91E8b": {
|
||||
"context": "app repository",
|
||||
"string": "Repository"
|
||||
},
|
||||
"fEfCtO": {
|
||||
"context": "voucher discount type",
|
||||
"string": "Percentage"
|
||||
|
@ -5703,6 +5778,10 @@
|
|||
"gxPjIQ": {
|
||||
"string": "Are you sure you want to delete {email} from staff members?"
|
||||
},
|
||||
"gygOA1": {
|
||||
"context": "button",
|
||||
"string": "Deactivate"
|
||||
},
|
||||
"gz9v22": {
|
||||
"context": "PluginChannelConfigurationCell channel title",
|
||||
"string": "Per channel"
|
||||
|
@ -6146,6 +6225,10 @@
|
|||
"context": "channel publication date",
|
||||
"string": "Published since {date}"
|
||||
},
|
||||
"kYtxJ1": {
|
||||
"context": "delete app",
|
||||
"string": "Deleting {name}, you will remove installation of the app. If you are paying for app subscription, remember to unsubscribe from the app in Saleor Marketplace."
|
||||
},
|
||||
"kZfIl/": {
|
||||
"string": "These are general information about this Content Type."
|
||||
},
|
||||
|
@ -6451,6 +6534,10 @@
|
|||
"context": "money amount input label",
|
||||
"string": "Enter amount"
|
||||
},
|
||||
"nA2g2e": {
|
||||
"context": "label",
|
||||
"string": "External app"
|
||||
},
|
||||
"nEWp+k": {
|
||||
"context": "quantity of ordered products",
|
||||
"string": "Quantity"
|
||||
|
@ -6513,6 +6600,10 @@
|
|||
"context": "tax class rates list label when no countries are assigned",
|
||||
"string": "There are no countries using this tax class yet, use {tab} tab to assign tax rates."
|
||||
},
|
||||
"nioOBQ": {
|
||||
"context": "delete custom app",
|
||||
"string": "Deleting {name}, you will delete all the data and webhooks regarding this app."
|
||||
},
|
||||
"njBulj": {
|
||||
"context": "check to require attribute to have value",
|
||||
"string": "Value Required"
|
||||
|
@ -6575,6 +6666,10 @@
|
|||
"context": "order status",
|
||||
"string": "Unfulfilled"
|
||||
},
|
||||
"oGIPb7": {
|
||||
"context": "information",
|
||||
"string": "Coming soon ({releaseDate})"
|
||||
},
|
||||
"oHbgcK": {
|
||||
"context": "PageTypeDeleteWarningDialog title",
|
||||
"string": "Delete page {selectedTypesCount,plural,one{type} other{types}}"
|
||||
|
@ -6619,6 +6714,10 @@
|
|||
"oYGfnY": {
|
||||
"string": "ZIP / Postal code"
|
||||
},
|
||||
"ohQrzn": {
|
||||
"context": "button",
|
||||
"string": "Deploy to Vercel"
|
||||
},
|
||||
"oiuwOl": {
|
||||
"context": "button",
|
||||
"string": "Assign"
|
||||
|
@ -6752,6 +6851,10 @@
|
|||
"context": "card subtitle",
|
||||
"string": "Entered prices"
|
||||
},
|
||||
"puUvKO": {
|
||||
"context": "button",
|
||||
"string": "Install external app"
|
||||
},
|
||||
"puikeb": {
|
||||
"context": "button",
|
||||
"string": "Delete Address"
|
||||
|
@ -7857,6 +7960,10 @@
|
|||
"yT5zvU": {
|
||||
"string": "{counter,plural,one{Are you sure you want to delete this collection?} other{Are you sure you want to delete {displayQuantity} collections?}}"
|
||||
},
|
||||
"yZB41s": {
|
||||
"context": "section header",
|
||||
"string": "All Apps"
|
||||
},
|
||||
"yatGsm": {
|
||||
"context": "card title",
|
||||
"string": "Order Value"
|
||||
|
@ -7911,6 +8018,10 @@
|
|||
"context": "file attribute type",
|
||||
"string": "File"
|
||||
},
|
||||
"z2yYL3": {
|
||||
"context": "hint",
|
||||
"string": "Usually ends with /api/manifest"
|
||||
},
|
||||
"z8jo8h": {
|
||||
"context": "button",
|
||||
"string": "View products"
|
||||
|
|
|
@ -15,6 +15,7 @@ export const appsList: AppListItemFragment[] = [
|
|||
isActive: true,
|
||||
name: "app",
|
||||
type: AppTypeEnum.THIRDPARTY,
|
||||
version: "1.0.0",
|
||||
appUrl: null,
|
||||
manifestUrl: "http://localhost:3000/api/manifest",
|
||||
permissions: [
|
||||
|
@ -31,6 +32,7 @@ export const appsList: AppListItemFragment[] = [
|
|||
isActive: false,
|
||||
name: "app1",
|
||||
type: AppTypeEnum.THIRDPARTY,
|
||||
version: "1.0.0",
|
||||
appUrl: "http://localhost:3000",
|
||||
manifestUrl: "http://localhost:3000/api/manifest",
|
||||
permissions: [
|
||||
|
@ -55,6 +57,7 @@ export const customAppsList: AppListItemFragment[] = [
|
|||
isActive: true,
|
||||
name: "app custom",
|
||||
type: AppTypeEnum.LOCAL,
|
||||
version: "1.0.0",
|
||||
appUrl: null,
|
||||
manifestUrl: null,
|
||||
permissions: [
|
||||
|
|
|
@ -47,21 +47,7 @@ export const appFetchMutation = gql`
|
|||
mutation AppFetch($manifestUrl: String!) {
|
||||
appFetchManifest(manifestUrl: $manifestUrl) {
|
||||
manifest {
|
||||
identifier
|
||||
version
|
||||
about
|
||||
name
|
||||
appUrl
|
||||
configurationUrl
|
||||
tokenTargetUrl
|
||||
dataPrivacy
|
||||
dataPrivacyUrl
|
||||
homepageUrl
|
||||
supportUrl
|
||||
permissions {
|
||||
code
|
||||
name
|
||||
}
|
||||
...AppManifest
|
||||
}
|
||||
errors {
|
||||
...AppError
|
||||
|
|
|
@ -42,3 +42,5 @@ export const TableButtonWrapper = <T extends HTMLElement>({
|
|||
|
||||
return children;
|
||||
};
|
||||
TableButtonWrapper.displayName = "TableButtonWrapper";
|
||||
export default TableButtonWrapper;
|
||||
|
|
2
src/components/TableButtonWrapper/index.ts
Normal file
2
src/components/TableButtonWrapper/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./TableButtonWrapper";
|
||||
export * from "./TableButtonWrapper";
|
|
@ -1,44 +1,13 @@
|
|||
import { Avatar as MuiAvatar } from "@material-ui/core";
|
||||
import { ImageIcon, makeStyles } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import AvatarImage from "./AvatarImage";
|
||||
import { useAvatarStyles } from "./styles";
|
||||
|
||||
export const AVATAR_MARGIN = 40;
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
alignRight: {
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
avatar: {
|
||||
background: "none",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 2,
|
||||
color: "#bdbdbd",
|
||||
display: "inline-flex",
|
||||
padding: theme.spacing(0.5),
|
||||
},
|
||||
children: {
|
||||
alignSelf: "center",
|
||||
marginLeft: theme.spacing(2),
|
||||
width: "100%",
|
||||
},
|
||||
content: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
},
|
||||
root: {
|
||||
"&:not(first-child)": {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
paddingRight: theme.spacing(3),
|
||||
width: "1%",
|
||||
},
|
||||
}),
|
||||
{ name: "Avatar" },
|
||||
);
|
||||
|
||||
export interface AvatarProps {
|
||||
initials?: string;
|
||||
thumbnail?: string;
|
||||
alignRight?: boolean;
|
||||
avatarProps?: string;
|
||||
|
@ -49,11 +18,12 @@ export interface AvatarProps {
|
|||
const Avatar: React.FC<AvatarProps> = ({
|
||||
children,
|
||||
alignRight,
|
||||
initials,
|
||||
thumbnail,
|
||||
avatarProps,
|
||||
badge,
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const classes = useAvatarStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -62,16 +32,11 @@ const Avatar: React.FC<AvatarProps> = ({
|
|||
})}
|
||||
>
|
||||
{badge}
|
||||
{!thumbnail ? (
|
||||
<MuiAvatar className={clsx(classes.avatar, avatarProps)}>
|
||||
<ImageIcon color="primary" data-test-id="imageIcon" />
|
||||
</MuiAvatar>
|
||||
) : (
|
||||
<MuiAvatar
|
||||
className={clsx(classes.avatar, avatarProps)}
|
||||
src={thumbnail}
|
||||
<AvatarImage
|
||||
thumbnail={thumbnail}
|
||||
initials={initials}
|
||||
avatarProps={avatarProps}
|
||||
/>
|
||||
)}
|
||||
{!alignRight && <div className={classes.children}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
|
|
42
src/components/TableCellAvatar/AvatarImage.tsx
Normal file
42
src/components/TableCellAvatar/AvatarImage.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Avatar as MuiAvatar, Typography } from "@material-ui/core";
|
||||
import { ImageIcon } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { useAvatarImageStyles } from "./styles";
|
||||
|
||||
interface AvatarImageProps {
|
||||
initials?: string;
|
||||
thumbnail?: string;
|
||||
avatarProps?: string;
|
||||
}
|
||||
|
||||
const AvatarImage: React.FC<AvatarImageProps> = ({
|
||||
initials,
|
||||
thumbnail,
|
||||
avatarProps,
|
||||
}) => {
|
||||
const classes = useAvatarImageStyles();
|
||||
|
||||
if (!thumbnail && initials) {
|
||||
return (
|
||||
<MuiAvatar className={clsx(classes.avatar, avatarProps)}>
|
||||
<Typography variant="h3">{initials}</Typography>
|
||||
</MuiAvatar>
|
||||
);
|
||||
}
|
||||
|
||||
if (!thumbnail) {
|
||||
return (
|
||||
<MuiAvatar className={clsx(classes.avatar, avatarProps)}>
|
||||
<ImageIcon color="primary" data-test-id="imageIcon" />
|
||||
</MuiAvatar>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MuiAvatar className={clsx(classes.avatar, avatarProps)} src={thumbnail} />
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarImage;
|
|
@ -1,23 +1,10 @@
|
|||
import { TableCell } from "@material-ui/core";
|
||||
import { TableCellProps } from "@material-ui/core/TableCell";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import Avatar, { AvatarProps } from "./Avatar";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
"&:not(first-child)": {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
paddingRight: theme.spacing(3),
|
||||
width: "1%",
|
||||
},
|
||||
}),
|
||||
{ name: "TableCellAvatar" },
|
||||
);
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface TableCellAvatarProps
|
||||
extends TableCellProps,
|
||||
|
|
53
src/components/TableCellAvatar/styles.ts
Normal file
53
src/components/TableCellAvatar/styles.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
root: {
|
||||
"&:not(first-child)": {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
paddingRight: theme.spacing(3),
|
||||
width: "1%",
|
||||
},
|
||||
}),
|
||||
{ name: "TableCellAvatar" },
|
||||
);
|
||||
|
||||
export const useAvatarStyles = makeStyles(
|
||||
theme => ({
|
||||
alignRight: {
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
children: {
|
||||
alignSelf: "center",
|
||||
marginLeft: theme.spacing(2),
|
||||
width: "100%",
|
||||
},
|
||||
content: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
},
|
||||
root: {
|
||||
"&:not(first-child)": {
|
||||
paddingLeft: 0,
|
||||
},
|
||||
paddingRight: theme.spacing(3),
|
||||
width: "1%",
|
||||
},
|
||||
}),
|
||||
{ name: "Avatar" },
|
||||
);
|
||||
|
||||
export const useAvatarImageStyles = makeStyles(
|
||||
theme => ({
|
||||
avatar: {
|
||||
background: "none",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 2,
|
||||
color: theme.palette.grey[500],
|
||||
display: "inline-flex",
|
||||
padding: theme.spacing(0.5),
|
||||
},
|
||||
}),
|
||||
{ name: "AvatarImage" },
|
||||
);
|
|
@ -16,6 +16,13 @@ export const MARKETPLACE_SALEOR_APPS_JSON_PATH =
|
|||
export const MARKETPLACE_APP_TEMPLATE_GALLERY_PATH =
|
||||
window.__SALEOR_CONFIG__.APP_TEMPLATE_GALLERY_PATH;
|
||||
|
||||
export const getAppsConfig = () => ({
|
||||
marketplaceApiUri: window.__SALEOR_CONFIG__.APPS_MARKETPLACE_API_URI,
|
||||
tunnelUrlKeywords: window.__SALEOR_CONFIG__.APPS_TUNNEL_URL_KEYWORDS?.split(
|
||||
";",
|
||||
) || [".ngrok.io", ".saleor.live", ".trycloudflare.com"],
|
||||
});
|
||||
|
||||
export const DEFAULT_INITIAL_SEARCH_DATA: SearchVariables = {
|
||||
after: null,
|
||||
first: 20,
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
import { gql } from "@apollo/client";
|
||||
|
||||
export const appManifestFragment = gql`
|
||||
fragment AppManifest on Manifest {
|
||||
identifier
|
||||
version
|
||||
about
|
||||
name
|
||||
appUrl
|
||||
configurationUrl
|
||||
tokenTargetUrl
|
||||
dataPrivacy
|
||||
dataPrivacyUrl
|
||||
homepageUrl
|
||||
supportUrl
|
||||
permissions {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const appFragment = gql`
|
||||
fragment App on App {
|
||||
id
|
||||
|
@ -41,6 +61,7 @@ export const appListItemFragment = gql`
|
|||
type
|
||||
appUrl
|
||||
manifestUrl
|
||||
version
|
||||
permissions {
|
||||
...AppPermission
|
||||
}
|
||||
|
|
|
@ -5,6 +5,25 @@ import { gql } from '@apollo/client';
|
|||
import * as Apollo from '@apollo/client';
|
||||
import * as ApolloReactHooks from '@saleor/hooks/graphql';
|
||||
const defaultOptions = {} as const;
|
||||
export const AppManifestFragmentDoc = gql`
|
||||
fragment AppManifest on Manifest {
|
||||
identifier
|
||||
version
|
||||
about
|
||||
name
|
||||
appUrl
|
||||
configurationUrl
|
||||
tokenTargetUrl
|
||||
dataPrivacy
|
||||
dataPrivacyUrl
|
||||
homepageUrl
|
||||
supportUrl
|
||||
permissions {
|
||||
code
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const WebhookFragmentDoc = gql`
|
||||
fragment Webhook on Webhook {
|
||||
id
|
||||
|
@ -62,6 +81,7 @@ export const AppListItemFragmentDoc = gql`
|
|||
type
|
||||
appUrl
|
||||
manifestUrl
|
||||
version
|
||||
permissions {
|
||||
...AppPermission
|
||||
}
|
||||
|
@ -2921,27 +2941,14 @@ export const AppFetchDocument = gql`
|
|||
mutation AppFetch($manifestUrl: String!) {
|
||||
appFetchManifest(manifestUrl: $manifestUrl) {
|
||||
manifest {
|
||||
identifier
|
||||
version
|
||||
about
|
||||
name
|
||||
appUrl
|
||||
configurationUrl
|
||||
tokenTargetUrl
|
||||
dataPrivacy
|
||||
dataPrivacyUrl
|
||||
homepageUrl
|
||||
supportUrl
|
||||
permissions {
|
||||
code
|
||||
name
|
||||
}
|
||||
...AppManifest
|
||||
}
|
||||
errors {
|
||||
...AppError
|
||||
}
|
||||
}
|
||||
}
|
||||
${AppManifestFragmentDoc}
|
||||
${AppErrorFragmentDoc}`;
|
||||
export type AppFetchMutationFn = Apollo.MutationFunction<Types.AppFetchMutation, Types.AppFetchMutationVariables>;
|
||||
|
||||
|
|
|
@ -6332,7 +6332,7 @@ export type AppsListQueryVariables = Exact<{
|
|||
}>;
|
||||
|
||||
|
||||
export type AppsListQuery = { __typename: 'Query', apps: { __typename: 'AppCountableConnection', totalCount: number | null, pageInfo: { __typename: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null, endCursor: string | null }, edges: Array<{ __typename: 'AppCountableEdge', node: { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null } }> } | null };
|
||||
export type AppsListQuery = { __typename: 'Query', apps: { __typename: 'AppCountableConnection', totalCount: number | null, pageInfo: { __typename: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null, endCursor: string | null }, edges: Array<{ __typename: 'AppCountableEdge', node: { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, version: string | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null } }> } | null };
|
||||
|
||||
export type AppsInstallationsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
@ -7053,9 +7053,11 @@ export type FileUploadMutation = { __typename: 'Mutation', fileUpload: { __typen
|
|||
|
||||
export type AddressFragment = { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } };
|
||||
|
||||
export type AppManifestFragment = { __typename: 'Manifest', identifier: string, version: string, about: string | null, name: string, appUrl: string | null, configurationUrl: string | null, tokenTargetUrl: string | null, dataPrivacy: string | null, dataPrivacyUrl: string | null, homepageUrl: string | null, supportUrl: string | null, permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null };
|
||||
|
||||
export type AppFragment = { __typename: 'App', id: string, name: string | null, created: any | null, isActive: boolean | null, type: AppTypeEnum | null, homepageUrl: string | null, appUrl: string | null, manifestUrl: string | null, configurationUrl: string | null, supportUrl: string | null, version: string | null, accessToken: string | null, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, tokens: Array<{ __typename: 'AppToken', authToken: string | null, id: string, name: string | null }> | null, webhooks: Array<{ __typename: 'Webhook', id: string, name: string, isActive: boolean, app: { __typename: 'App', id: string, name: string | null } }> | null };
|
||||
|
||||
export type AppListItemFragment = { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null };
|
||||
export type AppListItemFragment = { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, version: string | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null };
|
||||
|
||||
export type AppPermissionFragment = { __typename: 'Permission', name: string, code: PermissionEnum };
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface PageInfo {
|
||||
endCursor: string;
|
||||
endCursor: string | null;
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
startCursor: string;
|
||||
startCursor: string | null;
|
||||
}
|
||||
|
||||
export interface PaginationState {
|
||||
|
@ -91,11 +91,14 @@ export function useSectionLocalPaginationState(
|
|||
function useLocalPaginator(
|
||||
setPaginationState: (paginationState: PaginationState) => void,
|
||||
) {
|
||||
function paginate(pageInfo: PageInfo, paginationState: PaginationState) {
|
||||
function paginate(
|
||||
pageInfo: PageInfo | undefined,
|
||||
paginationState: PaginationState,
|
||||
) {
|
||||
const loadNextPage = () =>
|
||||
setPaginationState({
|
||||
...paginationState,
|
||||
after: pageInfo.endCursor,
|
||||
after: pageInfo?.endCursor,
|
||||
before: undefined,
|
||||
});
|
||||
|
||||
|
@ -103,7 +106,7 @@ function useLocalPaginator(
|
|||
setPaginationState({
|
||||
...paginationState,
|
||||
after: undefined,
|
||||
before: pageInfo.startCursor,
|
||||
before: pageInfo?.startCursor,
|
||||
});
|
||||
|
||||
const newPageInfo = pageInfo
|
||||
|
|
|
@ -102,8 +102,8 @@ export default usePaginator;
|
|||
export interface PaginatorContextValuesCommon {
|
||||
hasNextPage?: boolean;
|
||||
hasPreviousPage?: boolean;
|
||||
endCursor?: string;
|
||||
startCursor?: string;
|
||||
endCursor?: string | null;
|
||||
startCursor?: string | null;
|
||||
}
|
||||
|
||||
export type PaginatorContextValues = PaginatorContextValuesCommon &
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
SALEOR_APPS_PAGE_PATH: "<%= SALEOR_APPS_PAGE_PATH %>",
|
||||
SALEOR_APPS_JSON_PATH: "<%= SALEOR_APPS_JSON_PATH %>",
|
||||
APP_TEMPLATE_GALLERY_PATH: "<%= APP_TEMPLATE_GALLERY_PATH %>",
|
||||
APPS_MARKETPLACE_API_URI: "<%= APPS_MARKETPLACE_API_URI %>",
|
||||
APPS_TUNNEL_URL_KEYWORDS: "<%= APPS_TUNNEL_URL_KEYWORDS %>",
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
|
|
@ -55,6 +55,8 @@ import MarketplaceSection from "./marketplace";
|
|||
import { marketplaceUrl } from "./marketplace/urls";
|
||||
import NavigationSection from "./navigation";
|
||||
import { navigationSection } from "./navigation/urls";
|
||||
import NewAppsSection from "./new-apps";
|
||||
import { AppSections } from "./new-apps/urls";
|
||||
import { NotFound } from "./NotFound";
|
||||
import OrdersSection from "./orders";
|
||||
import PageSection from "./pages";
|
||||
|
@ -261,6 +263,10 @@ const Routes: React.FC = () => {
|
|||
path={appsSection}
|
||||
component={AppsSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
path={AppSections.appsSection}
|
||||
component={NewAppsSection}
|
||||
/>
|
||||
<SectionRoute
|
||||
permissions={[PermissionEnum.MANAGE_APPS]}
|
||||
path={marketplaceUrl}
|
||||
|
|
19
src/intl.ts
19
src/intl.ts
|
@ -186,6 +186,10 @@ export const commonMessages = defineMessages({
|
|||
id: "z9c6/C",
|
||||
defaultMessage: "Deprecated",
|
||||
},
|
||||
settings: {
|
||||
id: "D3idYv",
|
||||
defaultMessage: "Settings",
|
||||
},
|
||||
});
|
||||
|
||||
export const errorMessages = defineMessages({
|
||||
|
@ -333,6 +337,21 @@ export const buttonMessages = defineMessages({
|
|||
defaultMessage: "Undo",
|
||||
description: "button",
|
||||
},
|
||||
install: {
|
||||
id: "KDvRBi",
|
||||
defaultMessage: "Install",
|
||||
description: "button",
|
||||
},
|
||||
activate: {
|
||||
id: "+b3KCV",
|
||||
defaultMessage: "Activate",
|
||||
description: "button",
|
||||
},
|
||||
deactivate: {
|
||||
id: "gygOA1",
|
||||
defaultMessage: "Deactivate",
|
||||
description: "button",
|
||||
},
|
||||
});
|
||||
|
||||
export const sectionNames = defineMessages({
|
||||
|
|
39
src/new-apps/components/AllAppList/AllAppList.tsx
Normal file
39
src/new-apps/components/AllAppList/AllAppList.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Skeleton } from "@material-ui/lab";
|
||||
import { GetV2SaleorAppsResponse } from "@saleor/new-apps/marketplace.types";
|
||||
import React from "react";
|
||||
|
||||
import AppListCard from "../AppListCard";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface AllAppListProps {
|
||||
appList?: GetV2SaleorAppsResponse.SaleorApp[];
|
||||
navigateToAppInstallPage?: (manifestUrl: string) => void;
|
||||
navigateToVercelDeploymentPage?: (vercelDeploymentUrl: string) => void;
|
||||
}
|
||||
|
||||
const AllAppList: React.FC<AllAppListProps> = ({
|
||||
appList,
|
||||
navigateToAppInstallPage,
|
||||
navigateToVercelDeploymentPage,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
if (!appList) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.appListWrapper}>
|
||||
{appList.map(app => (
|
||||
<AppListCard
|
||||
key={app.name.en}
|
||||
app={app}
|
||||
navigateToAppInstallPage={navigateToAppInstallPage}
|
||||
navigateToVercelDeploymentPage={navigateToVercelDeploymentPage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllAppList;
|
2
src/new-apps/components/AllAppList/index.ts
Normal file
2
src/new-apps/components/AllAppList/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AllAppList";
|
||||
export { default } from "./AllAppList";
|
15
src/new-apps/components/AllAppList/styles.ts
Normal file
15
src/new-apps/components/AllAppList/styles.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
appListWrapper: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(400px, 1fr))`,
|
||||
gap: theme.spacing(2),
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "AllAppList",
|
||||
},
|
||||
);
|
|
@ -0,0 +1,18 @@
|
|||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import AppActivateDialog, { AppActivateDialogProps } from "./AppActivateDialog";
|
||||
|
||||
const props: AppActivateDialogProps = {
|
||||
confirmButtonState: "default",
|
||||
name: "App",
|
||||
onClose: () => undefined,
|
||||
onConfirm: () => undefined,
|
||||
open: true,
|
||||
};
|
||||
|
||||
storiesOf("Views / Apps / Activate app", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <AppActivateDialog {...props} />)
|
||||
.add("unnamed app", () => <AppActivateDialog {...props} name={null} />);
|
|
@ -0,0 +1,72 @@
|
|||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
import AppActivateDialog from "./AppActivateDialog";
|
||||
import msgs from "./messages";
|
||||
|
||||
describe("Apps AppActivateDialog", () => {
|
||||
it("displays action text with app name when app name passed", () => {
|
||||
// Arrange
|
||||
const name = "Test App";
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppActivateDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={name}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.activateNamedApp.defaultMessage.replace(
|
||||
"{name}",
|
||||
name,
|
||||
);
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
});
|
||||
|
||||
it("displays action text without app name when app name is empty", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppActivateDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={""}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.activateApp.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
});
|
||||
|
||||
it("displays action text without app name when app name is null", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppActivateDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={null}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.activateApp.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
import { DialogContentText } from "@material-ui/core";
|
||||
import ActionDialog from "@saleor/components/ActionDialog";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||
import { getStringOrPlaceholder } from "@saleor/misc";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import msgs from "./messages";
|
||||
|
||||
export interface AppActivateDialogProps {
|
||||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
open: boolean;
|
||||
name?: string | null;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const AppActivateDialog: React.FC<AppActivateDialogProps> = ({
|
||||
confirmButtonState,
|
||||
open,
|
||||
name,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const missingName = name === null || name === "";
|
||||
|
||||
const getMainText = () => {
|
||||
if (missingName) {
|
||||
return intl.formatMessage(msgs.activateApp);
|
||||
}
|
||||
return intl.formatMessage(msgs.activateNamedApp, {
|
||||
name: <strong>{getStringOrPlaceholder(name)}</strong>,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionDialog
|
||||
confirmButtonLabel={intl.formatMessage(buttonMessages.activate)}
|
||||
confirmButtonState={confirmButtonState}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
title={intl.formatMessage(msgs.activateAppTitle)}
|
||||
variant="default"
|
||||
>
|
||||
<DialogContentText data-test-id="dialog-content">
|
||||
{getMainText()}
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
);
|
||||
};
|
||||
AppActivateDialog.displayName = "AppActivateDialog";
|
||||
export default AppActivateDialog;
|
2
src/new-apps/components/AppActivateDialog/index.ts
Normal file
2
src/new-apps/components/AppActivateDialog/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AppActivateDialog";
|
||||
export { default } from "./AppActivateDialog";
|
21
src/new-apps/components/AppActivateDialog/messages.ts
Normal file
21
src/new-apps/components/AppActivateDialog/messages.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export default defineMessages({
|
||||
activateAppTitle: {
|
||||
id: "YHNozE",
|
||||
defaultMessage: "Activate App",
|
||||
description: "dialog header",
|
||||
},
|
||||
activateApp: {
|
||||
id: "Q47ovw",
|
||||
defaultMessage:
|
||||
"Are you sure you want to activate this app? Activating will start gathering events.",
|
||||
description: "activate app",
|
||||
},
|
||||
activateNamedApp: {
|
||||
id: "JbUg2b",
|
||||
defaultMessage:
|
||||
"Are you sure you want to activate {name}? Activating will start gathering events.",
|
||||
description: "activate app",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import AppDeactivateDialog, {
|
||||
AppDeactivateDialogProps,
|
||||
} from "./AppDeactivateDialog";
|
||||
|
||||
const props: AppDeactivateDialogProps = {
|
||||
confirmButtonState: "default",
|
||||
name: "App",
|
||||
onClose: () => undefined,
|
||||
onConfirm: () => undefined,
|
||||
open: true,
|
||||
};
|
||||
|
||||
storiesOf("Views / Apps / Deactivate app", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <AppDeactivateDialog {...props} />)
|
||||
.add("unnamed app", () => <AppDeactivateDialog {...props} name={null} />);
|
|
@ -0,0 +1,123 @@
|
|||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
import AppDeactivateDialog from "./AppDeactivateDialog";
|
||||
import msgs from "./messages";
|
||||
|
||||
describe("Apps AppDeactivateDialog", () => {
|
||||
it("displays action text with app name when third-party app name passed", () => {
|
||||
// Arrange
|
||||
const name = "Test App";
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeactivateDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={name}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedActionText = msgs.deactivateNamedApp.defaultMessage.replace(
|
||||
"{name}",
|
||||
name,
|
||||
);
|
||||
const expectedBillingWarning = msgs.deactivateAppBillingInfo.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedActionText);
|
||||
expect(dialogContent).toHaveTextContent(expectedBillingWarning);
|
||||
});
|
||||
|
||||
it("displays action text without app name when third-party app name is empty", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeactivateDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={""}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deactivateApp.defaultMessage;
|
||||
const expectedBillingWarning = msgs.deactivateAppBillingInfo.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
expect(dialogContent).toHaveTextContent(expectedBillingWarning);
|
||||
});
|
||||
|
||||
it("displays action text without app name when third-party app name is null", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeactivateDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={null}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deactivateApp.defaultMessage;
|
||||
const expectedBillingWarning = msgs.deactivateAppBillingInfo.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
expect(dialogContent).toHaveTextContent(expectedBillingWarning);
|
||||
});
|
||||
|
||||
it("displays billing warning when app is marked explicitly as third-party", () => {
|
||||
// Arrange
|
||||
const name = "Test App";
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeactivateDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={name}
|
||||
thirdParty={true}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedBillingWarning = msgs.deactivateAppBillingInfo.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedBillingWarning);
|
||||
});
|
||||
|
||||
it("doesn't display billing warning when app is marked explicitly as not third-party", () => {
|
||||
// Arrange
|
||||
const name = "Test App";
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeactivateDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={name}
|
||||
thirdParty={false}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const notExpectedBillingWarning =
|
||||
msgs.deactivateAppBillingInfo.defaultMessage;
|
||||
expect(dialogContent).not.toHaveTextContent(notExpectedBillingWarning);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import { DialogContentText } from "@material-ui/core";
|
||||
import ActionDialog from "@saleor/components/ActionDialog";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||
import { getStringOrPlaceholder } from "@saleor/misc";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import msgs from "./messages";
|
||||
|
||||
export interface AppDeactivateDialogProps {
|
||||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
open: boolean;
|
||||
name?: string | null;
|
||||
thirdParty?: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const AppDeactivateDialog: React.FC<AppDeactivateDialogProps> = ({
|
||||
confirmButtonState,
|
||||
open,
|
||||
name,
|
||||
thirdParty = true,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const missingName = name === null || name === "";
|
||||
|
||||
const getMainText = () => {
|
||||
if (missingName) {
|
||||
return intl.formatMessage(msgs.deactivateApp);
|
||||
}
|
||||
return intl.formatMessage(msgs.deactivateNamedApp, {
|
||||
name: <strong>{getStringOrPlaceholder(name)}</strong>,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionDialog
|
||||
confirmButtonLabel={intl.formatMessage(buttonMessages.deactivate)}
|
||||
confirmButtonState={confirmButtonState}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
title={intl.formatMessage(msgs.deactivateAppTitle)}
|
||||
variant="delete"
|
||||
>
|
||||
<DialogContentText data-test-id="dialog-content">
|
||||
{getMainText()}
|
||||
{thirdParty && (
|
||||
<>
|
||||
{" "}
|
||||
<FormattedMessage {...msgs.deactivateAppBillingInfo} />
|
||||
</>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
);
|
||||
};
|
||||
AppDeactivateDialog.displayName = "AppDeactivateDialog";
|
||||
export default AppDeactivateDialog;
|
2
src/new-apps/components/AppDeactivateDialog/index.ts
Normal file
2
src/new-apps/components/AppDeactivateDialog/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AppDeactivateDialog";
|
||||
export { default } from "./AppDeactivateDialog";
|
26
src/new-apps/components/AppDeactivateDialog/messages.ts
Normal file
26
src/new-apps/components/AppDeactivateDialog/messages.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export default defineMessages({
|
||||
deactivateAppTitle: {
|
||||
id: "yMi8I8",
|
||||
defaultMessage: "Dectivate App",
|
||||
description: "dialog header",
|
||||
},
|
||||
deactivateApp: {
|
||||
id: "Xu4ech",
|
||||
defaultMessage:
|
||||
"Are you sure you want to disable this app? Your data will be kept until you reactivate the app.",
|
||||
description: "deactivate app",
|
||||
},
|
||||
deactivateNamedApp: {
|
||||
id: "O987DI",
|
||||
defaultMessage:
|
||||
"Are you sure you want to disable {name}? Your data will be kept until you reactivate the app.",
|
||||
description: "deactivate named app",
|
||||
},
|
||||
deactivateAppBillingInfo: {
|
||||
id: "94oZR0",
|
||||
defaultMessage: "You will be still billed for the app.",
|
||||
description: "deactivate app billing info",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import AppDeleteDialog, { AppDeleteDialogProps } from "./AppDeleteDialog";
|
||||
|
||||
const props: AppDeleteDialogProps = {
|
||||
confirmButtonState: "default",
|
||||
name: "App",
|
||||
onClose: () => undefined,
|
||||
onConfirm: () => undefined,
|
||||
open: true,
|
||||
type: "EXTERNAL",
|
||||
};
|
||||
|
||||
storiesOf("Views / Apps / Delete app", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <AppDeleteDialog {...props} />)
|
||||
.add("unnamed app", () => <AppDeleteDialog {...props} name={null} />);
|
154
src/new-apps/components/AppDeleteDialog/AppDeleteDialog.test.tsx
Normal file
154
src/new-apps/components/AppDeleteDialog/AppDeleteDialog.test.tsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
import AppDeleteDialog from "./AppDeleteDialog";
|
||||
import msgs from "./messages";
|
||||
|
||||
describe("Apps AppDeleteDialog", () => {
|
||||
it("displays external app action text with app name when external app with app name passed", () => {
|
||||
// Arrange
|
||||
const name = "Test App";
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={name}
|
||||
type="EXTERNAL"
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteNamedApp.defaultMessage.replace(
|
||||
"{name}",
|
||||
name,
|
||||
);
|
||||
const expectedQuestion = msgs.deleteAppQuestion.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
expect(dialogContent).toHaveTextContent(expectedQuestion);
|
||||
});
|
||||
|
||||
it("displays custom app action text with app name when custom app with app name passed", () => {
|
||||
// Arrange
|
||||
const name = "Test App";
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={name}
|
||||
type="CUSTOM"
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteLocalNamedApp.defaultMessage.replace(
|
||||
"{name}",
|
||||
name,
|
||||
);
|
||||
const expectedQuestion = msgs.deleteAppQuestion.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
expect(dialogContent).toHaveTextContent(expectedQuestion);
|
||||
});
|
||||
|
||||
it("displays external action text without app name when external app name is empty", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={""}
|
||||
type="EXTERNAL"
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteApp.defaultMessage;
|
||||
const expectedQuestion = msgs.deleteAppQuestion.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
expect(dialogContent).toHaveTextContent(expectedQuestion);
|
||||
});
|
||||
|
||||
it("displays custom action text without app name when custom app name is empty", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={""}
|
||||
type="CUSTOM"
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteLocalApp.defaultMessage;
|
||||
const expectedQuestion = msgs.deleteAppQuestion.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
expect(dialogContent).toHaveTextContent(expectedQuestion);
|
||||
});
|
||||
|
||||
it("displays external action text without app name when external app name is null", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={null}
|
||||
type="EXTERNAL"
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteApp.defaultMessage;
|
||||
const expectedQuestion = msgs.deleteAppQuestion.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
expect(dialogContent).toHaveTextContent(expectedQuestion);
|
||||
});
|
||||
|
||||
it("displays custom action text without app name when custom app name is null", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={null}
|
||||
type="CUSTOM"
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteLocalApp.defaultMessage;
|
||||
const expectedQuestion = msgs.deleteAppQuestion.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
expect(dialogContent).toHaveTextContent(expectedQuestion);
|
||||
});
|
||||
});
|
65
src/new-apps/components/AppDeleteDialog/AppDeleteDialog.tsx
Normal file
65
src/new-apps/components/AppDeleteDialog/AppDeleteDialog.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { DialogContentText } from "@material-ui/core";
|
||||
import ActionDialog from "@saleor/components/ActionDialog";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||
import { getStringOrPlaceholder } from "@saleor/misc";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import msgs from "./messages";
|
||||
|
||||
export interface AppDeleteDialogProps {
|
||||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
open: boolean;
|
||||
name?: string | null;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
type: "CUSTOM" | "EXTERNAL";
|
||||
}
|
||||
|
||||
const AppDeleteDialog: React.FC<AppDeleteDialogProps> = ({
|
||||
confirmButtonState,
|
||||
open,
|
||||
name,
|
||||
onClose,
|
||||
onConfirm,
|
||||
type,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const missingName = name === null || name === "";
|
||||
const isExternal = type === "EXTERNAL";
|
||||
|
||||
const getMainText = () => {
|
||||
if (missingName && isExternal) {
|
||||
return intl.formatMessage(msgs.deleteApp);
|
||||
}
|
||||
if (missingName) {
|
||||
return intl.formatMessage(msgs.deleteLocalApp);
|
||||
}
|
||||
if (isExternal) {
|
||||
return intl.formatMessage(msgs.deleteNamedApp, {
|
||||
name: <strong>{getStringOrPlaceholder(name)}</strong>,
|
||||
});
|
||||
}
|
||||
return intl.formatMessage(msgs.deleteLocalNamedApp, {
|
||||
name: <strong>{getStringOrPlaceholder(name)}</strong>,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionDialog
|
||||
confirmButtonState={confirmButtonState}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
title={intl.formatMessage(msgs.deleteAppTitle)}
|
||||
variant="delete"
|
||||
>
|
||||
<DialogContentText data-test-id="dialog-content">
|
||||
{getMainText()} <FormattedMessage {...msgs.deleteAppQuestion} />
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
);
|
||||
};
|
||||
AppDeleteDialog.displayName = "AppDeleteDialog";
|
||||
export default AppDeleteDialog;
|
2
src/new-apps/components/AppDeleteDialog/index.ts
Normal file
2
src/new-apps/components/AppDeleteDialog/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AppDeleteDialog";
|
||||
export { default } from "./AppDeleteDialog";
|
38
src/new-apps/components/AppDeleteDialog/messages.ts
Normal file
38
src/new-apps/components/AppDeleteDialog/messages.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export default defineMessages({
|
||||
deleteAppTitle: {
|
||||
id: "zQX6xO",
|
||||
defaultMessage: "Delete App",
|
||||
description: "dialog header",
|
||||
},
|
||||
deleteApp: {
|
||||
id: "2rJcFa",
|
||||
defaultMessage:
|
||||
"Deleting this app, you will remove installation of the app. If you are paying for app subscription, remember to unsubscribe from the app in Saleor Marketplace.",
|
||||
description: "delete app",
|
||||
},
|
||||
deleteLocalApp: {
|
||||
id: "0vsMRq",
|
||||
defaultMessage:
|
||||
"Deleting this app, you will delete all the data and webhooks regarding this app.",
|
||||
description: "delete custom app",
|
||||
},
|
||||
deleteNamedApp: {
|
||||
id: "kYtxJ1",
|
||||
defaultMessage:
|
||||
"Deleting {name}, you will remove installation of the app. If you are paying for app subscription, remember to unsubscribe from the app in Saleor Marketplace.",
|
||||
description: "delete app",
|
||||
},
|
||||
deleteLocalNamedApp: {
|
||||
id: "nioOBQ",
|
||||
defaultMessage:
|
||||
"Deleting {name}, you will delete all the data and webhooks regarding this app.",
|
||||
description: "delete custom app",
|
||||
},
|
||||
deleteAppQuestion: {
|
||||
id: "6hLZNA",
|
||||
defaultMessage: "Are you sure you want to delete this app?",
|
||||
description: "delete app",
|
||||
},
|
||||
});
|
147
src/new-apps/components/AppListCard/AppListCard.test.tsx
Normal file
147
src/new-apps/components/AppListCard/AppListCard.test.tsx
Normal file
|
@ -0,0 +1,147 @@
|
|||
import { comingSoonApp, releasedApp } from "@saleor/new-apps/fixtures";
|
||||
import { GetV2SaleorAppsResponse } from "@saleor/new-apps/marketplace.types";
|
||||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
|
||||
import AppListCard from "./AppListCard";
|
||||
|
||||
describe("Apps AppListCard", () => {
|
||||
it("displays released app details when released app data passed", () => {
|
||||
// Arrange
|
||||
const integrationImages = releasedApp.integrations.map(
|
||||
integration => integration.logo.light.source,
|
||||
);
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppListCard app={releasedApp} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const name = screen.queryByText(releasedApp.name.en);
|
||||
const description = screen.queryByText(releasedApp.description.en);
|
||||
const images = screen.getAllByRole("img");
|
||||
const links = screen.getAllByRole("link");
|
||||
|
||||
// Assert
|
||||
expect(name).toBeTruthy();
|
||||
expect(description).toBeTruthy();
|
||||
const expectedImages = [releasedApp.logo.source, ...integrationImages];
|
||||
images.forEach(image =>
|
||||
expect(expectedImages).toContain(image.getAttribute("src")),
|
||||
);
|
||||
const expectedLinks = [
|
||||
releasedApp.privacyUrl,
|
||||
releasedApp.repositoryUrl,
|
||||
releasedApp.supportUrl,
|
||||
];
|
||||
links.forEach(link =>
|
||||
expect(expectedLinks).toContain(link.getAttribute("href")),
|
||||
);
|
||||
});
|
||||
|
||||
it("calls handlers when released app data passed and buttons clicked", async () => {
|
||||
// Arrange
|
||||
const navigateToAppInstallPage = jest.fn();
|
||||
const navigateToVercelDeploymentPage = jest.fn();
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppListCard
|
||||
app={releasedApp}
|
||||
navigateToAppInstallPage={navigateToAppInstallPage}
|
||||
navigateToVercelDeploymentPage={navigateToVercelDeploymentPage}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const installButton = screen.getByTestId("app-install-button");
|
||||
const deployToVercelButton = screen.getByTestId(
|
||||
"app-deploy-to-vercel-button",
|
||||
);
|
||||
|
||||
// Act
|
||||
await user.click(installButton);
|
||||
await user.click(deployToVercelButton);
|
||||
|
||||
// Assert
|
||||
expect(navigateToAppInstallPage).toBeCalledTimes(1);
|
||||
expect(navigateToVercelDeploymentPage).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it("displays coming soon app details when coming soon app data passed", () => {
|
||||
// Arrange
|
||||
const integrationImages = comingSoonApp.integrations.map(
|
||||
integration => integration.logo.light.source,
|
||||
);
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppListCard app={comingSoonApp} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const name = screen.queryByText(comingSoonApp.name.en);
|
||||
const description = screen.queryByText(comingSoonApp.description.en);
|
||||
const images = screen.getAllByRole("img");
|
||||
const links = screen.queryAllByRole("link");
|
||||
const releaseDate = screen.queryByText(comingSoonApp.releaseDate, {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(name).toBeTruthy();
|
||||
expect(description).toBeTruthy();
|
||||
const expectedImages = [comingSoonApp.logo.source, ...integrationImages];
|
||||
images.forEach(image =>
|
||||
expect(expectedImages).toContain(image.getAttribute("src")),
|
||||
);
|
||||
expect(links).toHaveLength(0);
|
||||
expect(releaseDate).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays placeholder initial when no released app logo passed", () => {
|
||||
// Arrange
|
||||
const app: GetV2SaleorAppsResponse.ReleasedSaleorApp = {
|
||||
...releasedApp,
|
||||
logo: {
|
||||
...releasedApp.logo,
|
||||
source: null,
|
||||
},
|
||||
};
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppListCard app={app} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const logo = screen.getByTestId("app-logo");
|
||||
const logoPlaceholder = within(logo).queryByTestId("app-logo-placeholder");
|
||||
const logoImage = within(logo).queryByRole("img");
|
||||
|
||||
// Assert
|
||||
expect(logoPlaceholder).toBeTruthy();
|
||||
expect(logoPlaceholder?.textContent).toBe(app.name.en[0]);
|
||||
expect(logoImage).toBeFalsy();
|
||||
});
|
||||
|
||||
it("displays placeholder initial when no coming soon app logo passed", () => {
|
||||
// Arrange
|
||||
const app: GetV2SaleorAppsResponse.ComingSoonSaleorApp = {
|
||||
...comingSoonApp,
|
||||
logo: {
|
||||
...comingSoonApp.logo,
|
||||
source: null,
|
||||
},
|
||||
};
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppListCard app={app} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const logo = screen.getByTestId("app-logo");
|
||||
const logoPlaceholder = within(logo).queryByTestId("app-logo-placeholder");
|
||||
const logoImage = within(logo).queryByRole("img");
|
||||
|
||||
// Assert
|
||||
expect(logoPlaceholder).toBeTruthy();
|
||||
expect(logoPlaceholder?.textContent).toBe(app.name.en[0]);
|
||||
expect(logoImage).toBeFalsy();
|
||||
});
|
||||
});
|
52
src/new-apps/components/AppListCard/AppListCard.tsx
Normal file
52
src/new-apps/components/AppListCard/AppListCard.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Card, CardContent } from "@material-ui/core";
|
||||
import { GetV2SaleorAppsResponse } from "@saleor/new-apps/marketplace.types";
|
||||
import { getAppDetails } from "@saleor/new-apps/utils";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import AppListCardActions from "./AppListCardActions";
|
||||
import AppListCardDescription from "./AppListCardDescription";
|
||||
import AppListCardIntegrations from "./AppListCardIntegrations";
|
||||
import AppListCardLinks from "./AppListCardLinks";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface AppListCardProps {
|
||||
app: GetV2SaleorAppsResponse.SaleorApp;
|
||||
navigateToAppInstallPage?: (manifestUrl: string) => void;
|
||||
navigateToVercelDeploymentPage?: (vercelDeploymentUrl: string) => void;
|
||||
}
|
||||
|
||||
const AppListCard: React.FC<AppListCardProps> = ({
|
||||
app,
|
||||
navigateToAppInstallPage,
|
||||
navigateToVercelDeploymentPage,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const intl = useIntl();
|
||||
|
||||
const details = getAppDetails(
|
||||
intl,
|
||||
app,
|
||||
navigateToAppInstallPage,
|
||||
navigateToVercelDeploymentPage,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className={classes.card}>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<AppListCardDescription app={app} />
|
||||
<AppListCardLinks links={details.links} />
|
||||
<AppListCardIntegrations app={app} />
|
||||
</CardContent>
|
||||
<AppListCardActions
|
||||
releaseDate={details.releaseDate}
|
||||
installHandler={details.installHandler}
|
||||
vercelDeployHandler={details.vercelDeployHandler}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
AppListCard.displayName = "AppListCard";
|
||||
export default AppListCard;
|
65
src/new-apps/components/AppListCard/AppListCardActions.tsx
Normal file
65
src/new-apps/components/AppListCard/AppListCardActions.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { CardActions, Typography } from "@material-ui/core";
|
||||
import { Button } from "@saleor/components/Button";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { messages } from "./messages";
|
||||
import { useActionsStyles } from "./styles";
|
||||
|
||||
interface AppListCardActionsProps {
|
||||
releaseDate: string | undefined;
|
||||
installHandler?: () => void;
|
||||
vercelDeployHandler?: () => void;
|
||||
}
|
||||
|
||||
const AppListCardActions: React.FC<AppListCardActionsProps> = ({
|
||||
releaseDate,
|
||||
installHandler,
|
||||
vercelDeployHandler,
|
||||
}) => {
|
||||
const classes = useActionsStyles();
|
||||
|
||||
if (!installHandler && !vercelDeployHandler && !releaseDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Hr />
|
||||
<CardActions className={classes.cardActions}>
|
||||
{vercelDeployHandler && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={vercelDeployHandler}
|
||||
data-test-id="app-deploy-to-vercel-button"
|
||||
>
|
||||
<FormattedMessage {...messages.deployToVercel} />
|
||||
</Button>
|
||||
)}
|
||||
{installHandler && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={installHandler}
|
||||
data-test-id="app-install-button"
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.install} />
|
||||
</Button>
|
||||
)}
|
||||
{releaseDate && (
|
||||
<Typography className={classes.releaseDate}>
|
||||
<FormattedMessage
|
||||
{...messages.releaseComingSoon}
|
||||
values={{
|
||||
releaseDate,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</CardActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
AppListCardActions.displayName = "AppListCardActions";
|
||||
export default AppListCardActions;
|
|
@ -0,0 +1,44 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { GetV2SaleorAppsResponse } from "@saleor/new-apps/marketplace.types";
|
||||
import React from "react";
|
||||
|
||||
import { useDescriptionStyles } from "./styles";
|
||||
|
||||
interface AppListCardDescriptionProps {
|
||||
app: GetV2SaleorAppsResponse.SaleorApp;
|
||||
}
|
||||
|
||||
const AppListCardDescription: React.FC<AppListCardDescriptionProps> = ({
|
||||
app,
|
||||
}) => {
|
||||
const classes = useDescriptionStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.cardToolbar}>
|
||||
<div
|
||||
className={classes.logo}
|
||||
style={{
|
||||
backgroundColor: app.logo.color,
|
||||
}}
|
||||
data-test-id="app-logo"
|
||||
>
|
||||
{app.logo.source && <img src={app.logo.source} alt="App logo" />}
|
||||
{!app.logo.source && (
|
||||
<Typography variant="h2" data-test-id="app-logo-placeholder">
|
||||
{app.name.en[0] || ""}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<Typography className={classes.cardHeader} variant="h2">
|
||||
{app.name.en}
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography className={classes.description} variant="body1">
|
||||
{app.description.en}
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
};
|
||||
AppListCardDescription.displayName = "AppListCardDescription";
|
||||
export default AppListCardDescription;
|
|
@ -0,0 +1,48 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { useTheme } from "@saleor/macaw-ui";
|
||||
import { GetV2SaleorAppsResponse } from "@saleor/new-apps/marketplace.types";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { messages } from "./messages";
|
||||
import { useIntegrationsStyles } from "./styles";
|
||||
|
||||
interface AppListCardIntegrationsProps {
|
||||
app: GetV2SaleorAppsResponse.SaleorApp;
|
||||
}
|
||||
|
||||
const AppListCardIntegrations: React.FC<AppListCardIntegrationsProps> = ({
|
||||
app,
|
||||
}) => {
|
||||
const { themeType } = useTheme();
|
||||
const classes = useIntegrationsStyles();
|
||||
|
||||
if (!app.integrations.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography className={classes.listHeader} variant="h3">
|
||||
<FormattedMessage {...messages.integrations} />
|
||||
</Typography>
|
||||
<ul className={classes.logoList}>
|
||||
{app.integrations.map(integration => (
|
||||
<li className={classes.vendorLogo} key={integration.name}>
|
||||
<img
|
||||
title={integration.name}
|
||||
src={
|
||||
themeType === "dark"
|
||||
? integration.logo.dark.source
|
||||
: integration.logo.light.source
|
||||
}
|
||||
alt={integration.name}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
AppListCardIntegrations.displayName = "AppListCardIntegrations";
|
||||
export default AppListCardIntegrations;
|
34
src/new-apps/components/AppListCard/AppListCardLinks.tsx
Normal file
34
src/new-apps/components/AppListCard/AppListCardLinks.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import Link from "@saleor/components/Link";
|
||||
import { AppLink } from "@saleor/new-apps/types";
|
||||
import React from "react";
|
||||
|
||||
import { useLinksStyles } from "./styles";
|
||||
|
||||
interface AppListCardLinksProps {
|
||||
links: AppLink[];
|
||||
}
|
||||
|
||||
const AppListCardLinks: React.FC<AppListCardLinksProps> = ({ links }) => {
|
||||
const classes = useLinksStyles();
|
||||
|
||||
if (!links.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={classes.linkList}>
|
||||
{links.map(link => (
|
||||
<li key={link.name}>
|
||||
<Typography>
|
||||
<Link href={link.url} target="_blank">
|
||||
{link.name}
|
||||
</Link>
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
AppListCardLinks.displayName = "AppListCardLinks";
|
||||
export default AppListCardLinks;
|
2
src/new-apps/components/AppListCard/index.ts
Normal file
2
src/new-apps/components/AppListCard/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AppListCard";
|
||||
export { default } from "./AppListCard";
|
19
src/new-apps/components/AppListCard/messages.ts
Normal file
19
src/new-apps/components/AppListCard/messages.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
integrations: {
|
||||
id: "MaTR88",
|
||||
defaultMessage: "Integrations",
|
||||
description: "header",
|
||||
},
|
||||
deployToVercel: {
|
||||
id: "ohQrzn",
|
||||
defaultMessage: "Deploy to Vercel",
|
||||
description: "button",
|
||||
},
|
||||
releaseComingSoon: {
|
||||
id: "oGIPb7",
|
||||
defaultMessage: "Coming soon ({releaseDate})",
|
||||
description: "information",
|
||||
},
|
||||
});
|
111
src/new-apps/components/AppListCard/styles.ts
Normal file
111
src/new-apps/components/AppListCard/styles.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { alpha } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
() => ({
|
||||
card: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
cardContent: {
|
||||
height: "100%",
|
||||
},
|
||||
}),
|
||||
{ name: "AppListCard" },
|
||||
);
|
||||
|
||||
export const useDescriptionStyles = makeStyles(
|
||||
theme => ({
|
||||
cardHeader: {
|
||||
fontWeight: 600,
|
||||
fontSize: 20,
|
||||
fontFamily: "sans-serif",
|
||||
},
|
||||
cardToolbar: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
logo: {
|
||||
color: "#fff",
|
||||
width: 50,
|
||||
overflow: "hidden",
|
||||
height: 50,
|
||||
marginRight: 20,
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
"& img": {
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
color: theme.palette.saleor.main[3],
|
||||
margin: theme.spacing(3, 0, 1, 0),
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "AppListCardDescription",
|
||||
},
|
||||
);
|
||||
|
||||
export const useLinksStyles = makeStyles(
|
||||
theme => ({
|
||||
linkList: {
|
||||
display: "flex",
|
||||
gap: 10,
|
||||
margin: theme.spacing(1, 0, 4, 0),
|
||||
padding: 0,
|
||||
listStyleType: "none",
|
||||
},
|
||||
}),
|
||||
{ name: "AppListCardLinks" },
|
||||
);
|
||||
|
||||
export const useIntegrationsStyles = makeStyles(
|
||||
theme => ({
|
||||
listHeader: {
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: theme.palette.saleor.main[3],
|
||||
margin: theme.spacing(2, 0),
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
logoList: {
|
||||
display: "flex",
|
||||
gap: theme.spacing(2),
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
vendorLogo: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
"& img": {
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
},
|
||||
background:
|
||||
theme.palette.type === "light"
|
||||
? "transparent"
|
||||
: alpha(theme.palette.saleor.generic.light, 0.3),
|
||||
},
|
||||
}),
|
||||
{ name: "AppListCardIntegrations" },
|
||||
);
|
||||
|
||||
export const useActionsStyles = makeStyles(
|
||||
theme => ({
|
||||
cardActions: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
padding: theme.spacing(2, 4),
|
||||
minHeight: theme.spacing(9),
|
||||
},
|
||||
releaseDate: {
|
||||
color: theme.palette.saleor.main[3],
|
||||
},
|
||||
}),
|
||||
{ name: "AppListCardActions" },
|
||||
);
|
23
src/new-apps/components/AppListPage/AppListPage.stories.tsx
Normal file
23
src/new-apps/components/AppListPage/AppListPage.stories.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { installedAppsList } from "@saleor/new-apps/fixtures";
|
||||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import AppListPage, { AppListPageProps } from "./AppListPage";
|
||||
|
||||
const props: AppListPageProps = {
|
||||
disabled: false,
|
||||
installedApps: installedAppsList,
|
||||
};
|
||||
|
||||
storiesOf("Views / New Apps / App List", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <AppListPage {...props} />)
|
||||
.add("empty", () => (
|
||||
<AppListPage
|
||||
{...props}
|
||||
installedApps={[]}
|
||||
installableMarketplaceApps={[]}
|
||||
comingSoonMarketplaceApps={[]}
|
||||
/>
|
||||
));
|
113
src/new-apps/components/AppListPage/AppListPage.tsx
Normal file
113
src/new-apps/components/AppListPage/AppListPage.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import Container from "@saleor/components/Container";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { AppUrls } from "@saleor/new-apps/urls";
|
||||
import { ListProps } from "@saleor/types";
|
||||
import React, { useCallback } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import AllAppList from "../AllAppList";
|
||||
import InstalledAppList from "../InstalledAppList";
|
||||
import { InstallWithManifestFormButton } from "../InstallWithManifestFormButton";
|
||||
import MarketplaceAlert from "../MarketplaceAlert";
|
||||
import SectionHeader from "../SectionHeader";
|
||||
import { messages } from "./messages";
|
||||
import { useStyles } from "./styles";
|
||||
import { AppListPageSections } from "./types";
|
||||
import {
|
||||
getVerifiedInstallableMarketplaceApps,
|
||||
getVerifiedInstalledApps,
|
||||
resolveSectionsAvailability,
|
||||
} from "./utils";
|
||||
|
||||
export interface AppListPageProps extends AppListPageSections, ListProps {
|
||||
marketplaceError?: Error;
|
||||
}
|
||||
|
||||
export const AppListPage: React.FC<AppListPageProps> = props => {
|
||||
const {
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
comingSoonMarketplaceApps,
|
||||
disabled,
|
||||
settings,
|
||||
marketplaceError,
|
||||
onUpdateListSettings,
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
const verifiedInstalledApps = getVerifiedInstalledApps(
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
);
|
||||
const verifiedInstallableMarketplaceApps = getVerifiedInstallableMarketplaceApps(
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
);
|
||||
const sectionsAvailability = resolveSectionsAvailability({
|
||||
...props,
|
||||
installableMarketplaceApps: verifiedInstallableMarketplaceApps,
|
||||
});
|
||||
const navigate = useNavigator();
|
||||
|
||||
const navigateToAppInstallPage = useCallback(
|
||||
(manifestUrl: string) => {
|
||||
navigate(AppUrls.resolveAppInstallUrl(manifestUrl));
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const navigateToVercelDeploymentPage = useCallback(
|
||||
(vercelDeploymentUrl: string) => {
|
||||
window.open(vercelDeploymentUrl, "_blank");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<PageHeader title={intl.formatMessage(sectionNames.apps)}>
|
||||
<InstallWithManifestFormButton onSubmitted={navigateToAppInstallPage} />
|
||||
</PageHeader>
|
||||
<Typography variant="body1">
|
||||
<FormattedMessage {...messages.installAppDescription} />
|
||||
</Typography>
|
||||
<div className={classes.appContent}>
|
||||
{sectionsAvailability.installed && (
|
||||
<>
|
||||
<SectionHeader title={intl.formatMessage(messages.installedApps)} />
|
||||
<InstalledAppList
|
||||
appList={verifiedInstalledApps}
|
||||
disabled={disabled}
|
||||
settings={settings}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<MarketplaceAlert error={marketplaceError} />
|
||||
{sectionsAvailability.all && !marketplaceError && (
|
||||
<>
|
||||
<SectionHeader title={intl.formatMessage(messages.allApps)} />
|
||||
<AllAppList
|
||||
appList={verifiedInstallableMarketplaceApps}
|
||||
navigateToAppInstallPage={navigateToAppInstallPage}
|
||||
navigateToVercelDeploymentPage={navigateToVercelDeploymentPage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{sectionsAvailability.comingSoon && !marketplaceError && (
|
||||
<>
|
||||
<SectionHeader
|
||||
title={intl.formatMessage(messages.comingSoonApps)}
|
||||
/>
|
||||
<AllAppList appList={comingSoonMarketplaceApps} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
AppListPage.displayName = "AppListPage";
|
||||
export default AppListPage;
|
2
src/new-apps/components/AppListPage/index.ts
Normal file
2
src/new-apps/components/AppListPage/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AppListPage";
|
||||
export { default } from "./AppListPage";
|
25
src/new-apps/components/AppListPage/messages.ts
Normal file
25
src/new-apps/components/AppListPage/messages.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
installAppDescription: {
|
||||
id: "ARfw53",
|
||||
defaultMessage:
|
||||
"Install apps created by Saleor team or install external apps.",
|
||||
description: "description",
|
||||
},
|
||||
installedApps: {
|
||||
id: "/PhkXi",
|
||||
defaultMessage: "Installed Apps",
|
||||
description: "section header",
|
||||
},
|
||||
allApps: {
|
||||
id: "yZB41s",
|
||||
defaultMessage: "All Apps",
|
||||
description: "section header",
|
||||
},
|
||||
comingSoonApps: {
|
||||
id: "VTq2gn",
|
||||
defaultMessage: "Coming Soon",
|
||||
description: "section header",
|
||||
},
|
||||
});
|
17
src/new-apps/components/AppListPage/styles.ts
Normal file
17
src/new-apps/components/AppListPage/styles.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
appContent: {
|
||||
margin: theme.spacing(4, 0),
|
||||
},
|
||||
sectionHeader: {
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: theme.palette.saleor.main[3],
|
||||
margin: theme.spacing(8, 0, 3, 0),
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
}),
|
||||
{ name: "AppListPage" },
|
||||
);
|
8
src/new-apps/components/AppListPage/types.ts
Normal file
8
src/new-apps/components/AppListPage/types.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { AppListItemFragment } from "@saleor/graphql";
|
||||
import { GetV2SaleorAppsResponse } from "@saleor/new-apps/marketplace.types";
|
||||
|
||||
export interface AppListPageSections {
|
||||
installedApps?: AppListItemFragment[];
|
||||
installableMarketplaceApps?: GetV2SaleorAppsResponse.ReleasedSaleorApp[];
|
||||
comingSoonMarketplaceApps?: GetV2SaleorAppsResponse.ComingSoonSaleorApp[];
|
||||
}
|
244
src/new-apps/components/AppListPage/utils.test.ts
Normal file
244
src/new-apps/components/AppListPage/utils.test.ts
Normal file
|
@ -0,0 +1,244 @@
|
|||
import {
|
||||
AppListItemFragment,
|
||||
AppTypeEnum,
|
||||
PermissionEnum,
|
||||
} from "@saleor/graphql";
|
||||
import {
|
||||
comingSoonApp,
|
||||
installedAppsList,
|
||||
releasedApp,
|
||||
} from "@saleor/new-apps/fixtures";
|
||||
|
||||
import { AppListPageSections } from "./types";
|
||||
import {
|
||||
getVerifiedInstallableMarketplaceApps,
|
||||
getVerifiedInstalledApps,
|
||||
resolveSectionsAvailability,
|
||||
} from "./utils";
|
||||
|
||||
describe("App List available sections util", () => {
|
||||
it("should return available app list sections when all data are passed", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
installedApps: installedAppsList,
|
||||
installableMarketplaceApps: [releasedApp],
|
||||
comingSoonMarketplaceApps: [comingSoonApp],
|
||||
};
|
||||
|
||||
// Act
|
||||
const sectionsAvailability = resolveSectionsAvailability(appSectionsData);
|
||||
|
||||
// Assert
|
||||
const expectedSectionsAvailability = {
|
||||
installed: true,
|
||||
all: true,
|
||||
comingSoon: true,
|
||||
};
|
||||
expect(sectionsAvailability).toEqual(expectedSectionsAvailability);
|
||||
});
|
||||
|
||||
it("should return no available app list sections when no data are passed", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
installedApps: [],
|
||||
installableMarketplaceApps: [],
|
||||
comingSoonMarketplaceApps: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const sectionsAvailability = resolveSectionsAvailability(appSectionsData);
|
||||
|
||||
// Assert
|
||||
const expectedSectionsAvailability = {
|
||||
installed: false,
|
||||
all: false,
|
||||
comingSoon: false,
|
||||
};
|
||||
expect(sectionsAvailability).toEqual(expectedSectionsAvailability);
|
||||
});
|
||||
});
|
||||
|
||||
describe("App List verified installed apps util", () => {
|
||||
it("should return installed apps list labeled as external properly when some of them are external", () => {
|
||||
// Arrange
|
||||
const installedApps: AppListItemFragment[] = [
|
||||
{
|
||||
__typename: "App",
|
||||
id: "QXBjPgE3Ng==",
|
||||
isActive: true,
|
||||
name: "app external",
|
||||
type: AppTypeEnum.THIRDPARTY,
|
||||
version: "1.0.0",
|
||||
appUrl: null,
|
||||
manifestUrl: "https://www.example.com/manifest",
|
||||
permissions: [
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_USERS,
|
||||
name: "Manage customers.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
__typename: "App",
|
||||
id: "QXBwOjE3Ng==",
|
||||
isActive: false,
|
||||
name: "app local",
|
||||
type: AppTypeEnum.THIRDPARTY,
|
||||
version: "1.0.0",
|
||||
appUrl: "http://localhost:3000",
|
||||
manifestUrl: "http://localhost:3000/api/manifest",
|
||||
permissions: [
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_ORDERS,
|
||||
name: "Manage orders.",
|
||||
},
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_USERS,
|
||||
name: "Manage customers.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const installableMarketplaceApps = [
|
||||
{
|
||||
name: {
|
||||
en: "Test app",
|
||||
},
|
||||
description: {
|
||||
en: "Test app description",
|
||||
},
|
||||
logo: {
|
||||
source: "https://www.example.com/logo",
|
||||
color: "#000000",
|
||||
},
|
||||
integrations: [],
|
||||
manifestUrl: "https://www.example.com/manifest",
|
||||
privacyUrl: "https://www.example.com/privacy",
|
||||
supportUrl: "https://www.example.com/support",
|
||||
repositoryUrl: "https://www.example.com/repository",
|
||||
vercelDeploymentUrl: "https://www.example.com/deployment",
|
||||
},
|
||||
];
|
||||
|
||||
// Act
|
||||
const verifiedInstalledApps = getVerifiedInstalledApps(
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
);
|
||||
|
||||
// Assert
|
||||
const expectedVerifiedInstalledApps = [
|
||||
{
|
||||
app: installedApps[0],
|
||||
isExternal: false,
|
||||
},
|
||||
{
|
||||
app: installedApps[1],
|
||||
isExternal: true,
|
||||
},
|
||||
];
|
||||
expect(verifiedInstalledApps).toEqual(expectedVerifiedInstalledApps);
|
||||
});
|
||||
});
|
||||
|
||||
describe("App List verified installable marketplace apps util", () => {
|
||||
it("should return filtered installable marketplace apps list when some of them are already installed", () => {
|
||||
// Arrange
|
||||
const installedApps: AppListItemFragment[] = [
|
||||
{
|
||||
__typename: "App",
|
||||
id: "QXBjPgE3Ng==",
|
||||
isActive: true,
|
||||
name: "app external",
|
||||
type: AppTypeEnum.THIRDPARTY,
|
||||
version: "1.0.0",
|
||||
appUrl: null,
|
||||
manifestUrl: "https://www.example.com/manifest",
|
||||
permissions: [
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_USERS,
|
||||
name: "Manage customers.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
__typename: "App",
|
||||
id: "QXBwOjE3Ng==",
|
||||
isActive: false,
|
||||
name: "app local",
|
||||
type: AppTypeEnum.THIRDPARTY,
|
||||
version: "1.0.0",
|
||||
appUrl: "http://localhost:3000",
|
||||
manifestUrl: "http://localhost:3000/api/manifest",
|
||||
permissions: [
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_ORDERS,
|
||||
name: "Manage orders.",
|
||||
},
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_USERS,
|
||||
name: "Manage customers.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const installableMarketplaceApps = [
|
||||
{
|
||||
name: {
|
||||
en: "Test app",
|
||||
},
|
||||
description: {
|
||||
en: "Test app description",
|
||||
},
|
||||
logo: {
|
||||
source: "https://www.example.com/logo",
|
||||
color: "#000000",
|
||||
},
|
||||
integrations: [],
|
||||
manifestUrl: "https://www.example.com/manifest",
|
||||
privacyUrl: "https://www.example.com/privacy",
|
||||
supportUrl: "https://www.example.com/support",
|
||||
repositoryUrl: "https://www.example.com/repository",
|
||||
vercelDeploymentUrl: "https://www.example.com/deployment",
|
||||
},
|
||||
{
|
||||
name: {
|
||||
en: "Test app",
|
||||
},
|
||||
description: {
|
||||
en: "Test app description",
|
||||
},
|
||||
logo: {
|
||||
source: "https://www.example-2.com/logo",
|
||||
color: "#000000",
|
||||
},
|
||||
integrations: [],
|
||||
manifestUrl: "https://www.example-2.com/manifest",
|
||||
privacyUrl: "https://www.example-2.com/privacy",
|
||||
supportUrl: "https://www.example-2.com/support",
|
||||
repositoryUrl: "https://www.example-2.com/repository",
|
||||
vercelDeploymentUrl: "https://www.example-2.com/deployment",
|
||||
},
|
||||
];
|
||||
|
||||
// Act
|
||||
const verifiedInstallableMarketplaceApps = getVerifiedInstallableMarketplaceApps(
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
);
|
||||
|
||||
// Assert
|
||||
const expectedVerifiedInstallableMarketplaceApps = [
|
||||
installableMarketplaceApps[1],
|
||||
];
|
||||
expect(verifiedInstallableMarketplaceApps).toEqual(
|
||||
expectedVerifiedInstallableMarketplaceApps,
|
||||
);
|
||||
});
|
||||
});
|
49
src/new-apps/components/AppListPage/utils.ts
Normal file
49
src/new-apps/components/AppListPage/utils.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { AppListItemFragment } from "@saleor/graphql";
|
||||
import { GetV2SaleorAppsResponse } from "@saleor/new-apps/marketplace.types";
|
||||
import { InstalledApp } from "@saleor/new-apps/types";
|
||||
|
||||
import { AppListPageSections } from "./types";
|
||||
|
||||
export const resolveSectionsAvailability = ({
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
comingSoonMarketplaceApps,
|
||||
}: AppListPageSections) => ({
|
||||
installed: !installedApps || !!installedApps.length,
|
||||
all: !installableMarketplaceApps || !!installableMarketplaceApps.length,
|
||||
comingSoon: !comingSoonMarketplaceApps || !!comingSoonMarketplaceApps.length,
|
||||
});
|
||||
|
||||
const isAppExternal = (
|
||||
manifestUrl: string | null,
|
||||
installableMarketplaceApps?: GetV2SaleorAppsResponse.ReleasedSaleorApp[],
|
||||
) =>
|
||||
!!manifestUrl &&
|
||||
!!installableMarketplaceApps?.every(app => app.manifestUrl !== manifestUrl);
|
||||
|
||||
export const getVerifiedInstalledApps = (
|
||||
installedApps?: AppListItemFragment[],
|
||||
installableMarketplaceApps?: GetV2SaleorAppsResponse.ReleasedSaleorApp[],
|
||||
): InstalledApp[] | undefined =>
|
||||
installedApps?.map(app => ({
|
||||
app,
|
||||
isExternal: isAppExternal(app.manifestUrl, installableMarketplaceApps),
|
||||
}));
|
||||
|
||||
/*
|
||||
* Temporary solution for checking if app is not installed.
|
||||
* Installed app list is paginated, it needs to be replace in the future with proper solution,
|
||||
* not relying on one page of installed apps list.
|
||||
*/
|
||||
const isAppNotInstalled = (
|
||||
manifestUrl: string,
|
||||
installedApps?: AppListItemFragment[],
|
||||
) => installedApps?.every(app => app.manifestUrl !== manifestUrl);
|
||||
|
||||
export const getVerifiedInstallableMarketplaceApps = (
|
||||
installedApps?: AppListItemFragment[],
|
||||
installableMarketplaceApps?: GetV2SaleorAppsResponse.ReleasedSaleorApp[],
|
||||
): GetV2SaleorAppsResponse.ReleasedSaleorApp[] | undefined =>
|
||||
installableMarketplaceApps?.filter(app =>
|
||||
isAppNotInstalled(app.manifestUrl, installedApps),
|
||||
);
|
|
@ -0,0 +1,57 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { CopyIcon, Tooltip } from "@saleor/macaw-ui";
|
||||
import { appsMessages } from "@saleor/new-apps/messages";
|
||||
import clsx from "clsx";
|
||||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface AppManifestTableDisplayProps {
|
||||
manifestUrl: string;
|
||||
}
|
||||
|
||||
const getAppDomainFromManifest = (manifest: string) => new URL(manifest).host;
|
||||
|
||||
export const AppManifestTableDisplay = ({
|
||||
manifestUrl,
|
||||
}: AppManifestTableDisplayProps) => {
|
||||
const styles = useStyles();
|
||||
const intl = useIntl();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={manifestUrl}
|
||||
header={intl.formatMessage(appsMessages.appManifestUrl)}
|
||||
>
|
||||
<Typography
|
||||
onMouseOut={() => setCopied(false)}
|
||||
className={styles.manifestText}
|
||||
onClick={e => {
|
||||
try {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
navigator.clipboard.writeText(manifestUrl);
|
||||
setCopied(true);
|
||||
} catch (e) {
|
||||
// Copy not supported, ignore
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getAppDomainFromManifest(manifestUrl)}
|
||||
{!!navigator.clipboard && (
|
||||
<CopyIcon
|
||||
className={clsx(styles.copyIcon, {
|
||||
[styles.copyIconColorful]: copied,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
AppManifestTableDisplay.displayName = "AppManifestTableDisplay";
|
||||
export default AppManifestTableDisplay;
|
2
src/new-apps/components/AppManifestTableDisplay/index.ts
Normal file
2
src/new-apps/components/AppManifestTableDisplay/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { AppManifestTableDisplay } from "./AppManifestTableDisplay";
|
||||
export { default } from "./AppManifestTableDisplay";
|
28
src/new-apps/components/AppManifestTableDisplay/styles.ts
Normal file
28
src/new-apps/components/AppManifestTableDisplay/styles.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
"@keyframes pulse": {
|
||||
from: { transform: "scale(1)" },
|
||||
to: { transform: "scale(1.2)" },
|
||||
},
|
||||
manifestText: {
|
||||
color: theme.palette.text.secondary,
|
||||
"&:hover svg": {
|
||||
visibility: "visible",
|
||||
},
|
||||
},
|
||||
copyIcon: {
|
||||
marginLeft: theme.spacing(1),
|
||||
visibility: "hidden",
|
||||
verticalAlign: "middle",
|
||||
transition: "0.2s",
|
||||
cursor: "pointer",
|
||||
},
|
||||
copyIconColorful: {
|
||||
color: theme.palette.primary.main,
|
||||
animation: "$pulse 0.2s",
|
||||
},
|
||||
}),
|
||||
{ name: "AppManifestTableDisplay" },
|
||||
);
|
36
src/new-apps/components/AppPermissions/AppPermissions.tsx
Normal file
36
src/new-apps/components/AppPermissions/AppPermissions.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { AppPermissionFragment } from "@saleor/graphql";
|
||||
import { IconButton, PermissionsIcon, Tooltip } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { messages } from "./messages";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface AppPermissionsProps {
|
||||
permissions?: AppPermissionFragment[] | null;
|
||||
}
|
||||
|
||||
export const AppPermissions: React.FC<AppPermissionsProps> = ({
|
||||
permissions,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
header={<FormattedMessage {...messages.appPermissions} />}
|
||||
title={
|
||||
<ul className={classes.list}>
|
||||
{permissions?.map(permission => (
|
||||
<li key={permission.code}>{permission.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<IconButton variant="secondary" color="primary">
|
||||
<PermissionsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
AppPermissions.displayName = "AppPermissions";
|
||||
export default AppPermissions;
|
2
src/new-apps/components/AppPermissions/index.ts
Normal file
2
src/new-apps/components/AppPermissions/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { AppPermissions } from "./AppPermissions";
|
||||
export { default } from "./AppPermissions";
|
9
src/new-apps/components/AppPermissions/messages.ts
Normal file
9
src/new-apps/components/AppPermissions/messages.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
appPermissions: {
|
||||
id: "xNfh4L",
|
||||
defaultMessage: "App permissions",
|
||||
description: "app permissions tooltip header",
|
||||
},
|
||||
});
|
11
src/new-apps/components/AppPermissions/styles.ts
Normal file
11
src/new-apps/components/AppPermissions/styles.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
() => ({
|
||||
list: {
|
||||
margin: 0,
|
||||
paddingLeft: "16px",
|
||||
},
|
||||
}),
|
||||
{ name: "AppPermissions" },
|
||||
);
|
|
@ -0,0 +1,65 @@
|
|||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
|
||||
import InstallWithManifestFormButton from "./InstallWithManifestFormButton";
|
||||
|
||||
describe("Apps InstallWithManifestFormButton", () => {
|
||||
it("submit form when valid manifest url value passed", async () => {
|
||||
// Arrange
|
||||
const submitHandler = jest.fn();
|
||||
render(
|
||||
<Wrapper>
|
||||
<InstallWithManifestFormButton onSubmitted={submitHandler} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const installButton = screen.getAllByTestId("add-app-from-manifest")[0];
|
||||
// Act
|
||||
await user.click(installButton);
|
||||
// Arrange
|
||||
const input = screen.getByRole("textbox");
|
||||
// Assert
|
||||
expect(input).not.toHaveValue();
|
||||
// Act
|
||||
await user.type(input, "https://example.com/manifest.json");
|
||||
// Assert
|
||||
expect(input).toHaveValue("https://example.com/manifest.json");
|
||||
// Arrange
|
||||
const submitButton = screen.getAllByTestId("install-app-from-manifest")[0];
|
||||
// Act
|
||||
await user.click(submitButton);
|
||||
// Assert
|
||||
expect(submitHandler).toBeCalledTimes(1);
|
||||
expect(submitHandler).toBeCalledWith("https://example.com/manifest.json");
|
||||
});
|
||||
|
||||
it("return error when invalid manifest url value passed", async () => {
|
||||
// Arrange
|
||||
const submitHandler = jest.fn();
|
||||
render(
|
||||
<Wrapper>
|
||||
<InstallWithManifestFormButton onSubmitted={submitHandler} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const installButton = screen.getAllByTestId("add-app-from-manifest")[0];
|
||||
// Act
|
||||
await user.click(installButton);
|
||||
// Arrange
|
||||
const input = screen.getByRole("textbox");
|
||||
// Assert
|
||||
expect(input).not.toHaveValue();
|
||||
// Act
|
||||
await user.type(input, "example wrong url");
|
||||
// Assert
|
||||
expect(input).toHaveValue("example wrong url");
|
||||
// Arrange
|
||||
const submitButton = screen.getAllByTestId("install-app-from-manifest")[0];
|
||||
// Act
|
||||
await user.click(submitButton);
|
||||
// Assert
|
||||
expect(submitHandler).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
import { TextField } from "@material-ui/core";
|
||||
import { Button } from "@saleor/components/Button";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import { appsMessages } from "@saleor/new-apps/messages";
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { messages } from "./messages";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface Props {
|
||||
onSubmitted(manifestUrl: string): void;
|
||||
}
|
||||
|
||||
export const InstallWithManifestFormButton: React.FC<Props> = ({
|
||||
onSubmitted,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const intl = useIntl();
|
||||
|
||||
const [inputOpened, setInputOpened] = useState(false);
|
||||
|
||||
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const form = new FormData(e.currentTarget);
|
||||
const inputValue = form.get("manifest-url") as string;
|
||||
|
||||
try {
|
||||
new URL(inputValue);
|
||||
|
||||
onSubmitted(inputValue);
|
||||
} catch (e) {
|
||||
console.error("Invalid URL from input. Should be validated by browser");
|
||||
}
|
||||
};
|
||||
|
||||
if (inputOpened) {
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<TextField
|
||||
data-test-id="manifest-url-input"
|
||||
required
|
||||
type="url"
|
||||
name="manifest-url"
|
||||
label={intl.formatMessage(appsMessages.appManifestUrl)}
|
||||
defaultValue=""
|
||||
helperText={intl.formatMessage(messages.appManifestUrlHint)}
|
||||
/>
|
||||
<Button
|
||||
size="medium"
|
||||
type="submit"
|
||||
className={styles.installButton}
|
||||
variant="primary"
|
||||
data-test-id="install-app-from-manifest"
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.install} />
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-test-id="add-app-from-manifest"
|
||||
onClick={() => setInputOpened(true)}
|
||||
>
|
||||
<FormattedMessage {...messages.installExternalApp} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
InstallWithManifestFormButton.displayName = "InstallWithManifestFormButton";
|
||||
export default InstallWithManifestFormButton;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./InstallWithManifestFormButton";
|
||||
export { default } from "./InstallWithManifestFormButton";
|
|
@ -0,0 +1,14 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
installExternalApp: {
|
||||
id: "puUvKO",
|
||||
defaultMessage: "Install external app",
|
||||
description: "button",
|
||||
},
|
||||
appManifestUrlHint: {
|
||||
id: "z2yYL3",
|
||||
defaultMessage: "Usually ends with /api/manifest",
|
||||
description: "hint",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
installButton: {
|
||||
marginLeft: theme.spacing(2),
|
||||
height: 52,
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "InstallWithManifestFormButton",
|
||||
},
|
||||
);
|
|
@ -0,0 +1,47 @@
|
|||
import { Table, TableBody, TableFooter, TableRow } from "@material-ui/core";
|
||||
import { Skeleton } from "@material-ui/lab";
|
||||
import { TablePaginationWithContext } from "@saleor/components/TablePagination";
|
||||
import { InstalledApp } from "@saleor/new-apps/types";
|
||||
import { ListProps } from "@saleor/types";
|
||||
import React from "react";
|
||||
|
||||
import InstalledAppListRow from "../InstalledAppListRow";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface InstalledAppListProps extends ListProps {
|
||||
appList?: InstalledApp[];
|
||||
}
|
||||
|
||||
const InstalledAppList: React.FC<InstalledAppListProps> = ({
|
||||
appList,
|
||||
disabled,
|
||||
settings,
|
||||
onUpdateListSettings,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
if (!appList) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table className={classes.table}>
|
||||
<TableBody>
|
||||
{appList.map(({ app, isExternal }) => (
|
||||
<InstalledAppListRow key={app.id} app={app} isExternal={isExternal} />
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePaginationWithContext
|
||||
disabled={disabled}
|
||||
settings={settings}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstalledAppList;
|
2
src/new-apps/components/InstalledAppList/index.ts
Normal file
2
src/new-apps/components/InstalledAppList/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./InstalledAppList";
|
||||
export { default } from "./InstalledAppList";
|
14
src/new-apps/components/InstalledAppList/messages.ts
Normal file
14
src/new-apps/components/InstalledAppList/messages.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
externalApp: {
|
||||
id: "nA2g2e",
|
||||
defaultMessage: "External app",
|
||||
description: "label",
|
||||
},
|
||||
tunnelDevelopment: {
|
||||
id: "RjT+Vo",
|
||||
defaultMessage: "Tunnel - development",
|
||||
description: "label",
|
||||
},
|
||||
});
|
10
src/new-apps/components/InstalledAppList/styles.ts
Normal file
10
src/new-apps/components/InstalledAppList/styles.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
table: {
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
}),
|
||||
{ name: "InstalledAppList" },
|
||||
);
|
|
@ -0,0 +1,196 @@
|
|||
import { getAppsConfig } from "@saleor/config";
|
||||
import { AppListContext, AppListContextValues } from "@saleor/new-apps/context";
|
||||
import { activeApp, inactiveApp } from "@saleor/new-apps/fixtures";
|
||||
import { InstalledApp } from "@saleor/new-apps/types";
|
||||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { MemoryRouter as Router } from "react-router-dom";
|
||||
|
||||
import InstalledAppListRow from "./InstalledAppListRow";
|
||||
|
||||
const Component = ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: InstalledApp;
|
||||
context: AppListContextValues;
|
||||
}) => (
|
||||
<Wrapper>
|
||||
<Router>
|
||||
<AppListContext.Provider value={context}>
|
||||
<InstalledAppListRow {...data} />
|
||||
</AppListContext.Provider>
|
||||
</Router>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
describe("Apps InstalledAppListRow", () => {
|
||||
it("displays app details when basic app data passed", () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
app: activeApp,
|
||||
isExternal: false,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const name = screen.queryByText(activeApp.name as string);
|
||||
const version = screen.queryByText(activeApp.version as string, {
|
||||
exact: false,
|
||||
});
|
||||
const manifestDomain = screen.queryByText(
|
||||
new URL(activeApp.manifestUrl as string).host,
|
||||
);
|
||||
const externalLabel = screen.queryByTestId("app-external-label");
|
||||
const tunnelLabel = screen.queryByTestId("app-tunnel-label");
|
||||
|
||||
// Assert
|
||||
expect(name).toBeTruthy();
|
||||
expect(version).toBeTruthy();
|
||||
expect(manifestDomain).toBeTruthy();
|
||||
expect(externalLabel).toBeFalsy();
|
||||
expect(tunnelLabel).toBeFalsy();
|
||||
});
|
||||
|
||||
it("displays external label when app is external", () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
app: activeApp,
|
||||
isExternal: true,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const externalLabel = screen.queryByTestId("app-external-label");
|
||||
|
||||
// Assert
|
||||
expect(externalLabel).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays tunnnel label when app is served via tunnnel", () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
const AppsConfig = getAppsConfig();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
app: {
|
||||
...activeApp,
|
||||
appUrl: `https://example${AppsConfig.tunnelUrlKeywords[0]}`,
|
||||
manifestUrl: `https://example${AppsConfig.tunnelUrlKeywords[0]}/api/manifest`,
|
||||
},
|
||||
isExternal: false,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const tunnelLabel = screen.queryByTestId("app-tunnel-label");
|
||||
|
||||
// Assert
|
||||
expect(tunnelLabel).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls handlers when active app data passed and buttons clicked", async () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
app: activeApp,
|
||||
isExternal: false,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const activeSwitch = within(
|
||||
screen.getByTestId("app-active-switch"),
|
||||
).getByRole("checkbox");
|
||||
const removeButton = screen.getByTestId("app-remove-button");
|
||||
|
||||
// Assert
|
||||
expect(activeSwitch).toBeChecked();
|
||||
|
||||
// Act
|
||||
await user.click(activeSwitch);
|
||||
await user.click(removeButton);
|
||||
|
||||
// Assert
|
||||
expect(deactivateApp).toHaveBeenCalledWith(activeApp.id);
|
||||
expect(deactivateApp).toHaveBeenCalledTimes(1);
|
||||
expect(activateApp).not.toHaveBeenCalled();
|
||||
expect(removeApp).toHaveBeenCalledWith(activeApp.id);
|
||||
expect(removeApp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls handlers when inactive app data passed and buttons clicked", async () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
app: inactiveApp,
|
||||
isExternal: false,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const activeSwitch = within(
|
||||
screen.getByTestId("app-active-switch"),
|
||||
).getByRole("checkbox");
|
||||
const removeButton = screen.getByTestId("app-remove-button");
|
||||
|
||||
// Assert
|
||||
expect(activeSwitch).not.toBeChecked();
|
||||
|
||||
// Act
|
||||
await user.click(activeSwitch);
|
||||
await user.click(removeButton);
|
||||
|
||||
// Assert
|
||||
expect(activateApp).toHaveBeenCalledWith(inactiveApp.id);
|
||||
expect(activateApp).toHaveBeenCalledTimes(1);
|
||||
expect(deactivateApp).not.toHaveBeenCalled();
|
||||
expect(removeApp).toHaveBeenCalledWith(inactiveApp.id);
|
||||
expect(removeApp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
import { Switch, TableCell, Typography } from "@material-ui/core";
|
||||
import { TableButtonWrapper } from "@saleor/components/TableButtonWrapper/TableButtonWrapper";
|
||||
import TableCellAvatar from "@saleor/components/TableCellAvatar";
|
||||
import TableRowLink from "@saleor/components/TableRowLink";
|
||||
import { DeleteIcon, IconButton, Pill } from "@saleor/macaw-ui";
|
||||
import { useAppListContext } from "@saleor/new-apps/context";
|
||||
import { InstalledApp } from "@saleor/new-apps/types";
|
||||
import { AppUrls } from "@saleor/new-apps/urls";
|
||||
import { isAppInTunnel } from "@saleor/new-apps/utils";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import AppManifestTableDisplay from "../AppManifestTableDisplay";
|
||||
import { AppPermissions } from "../AppPermissions";
|
||||
import { messages } from "./messages";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
export const InstalledAppListRow: React.FC<InstalledApp> = ({
|
||||
app,
|
||||
isExternal,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
const { activateApp, deactivateApp, removeApp } = useAppListContext();
|
||||
|
||||
const handleToggleActive = () => {
|
||||
if (app.isActive) {
|
||||
deactivateApp(app.id);
|
||||
} else {
|
||||
activateApp(app.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRowLink className={classes.row} href={AppUrls.resolveAppUrl(app.id)}>
|
||||
<TableCellAvatar
|
||||
initials={app.name?.[0]?.toUpperCase()}
|
||||
thumbnail={undefined}
|
||||
className={clsx(classes.col, classes.colLogo)}
|
||||
>
|
||||
<div className={classes.mainContent}>
|
||||
<Typography variant="body1" className={classes.name}>
|
||||
{app.name}
|
||||
</Typography>
|
||||
<Typography variant="body1" className={classes.version}>
|
||||
{`v${app.version}`}
|
||||
</Typography>
|
||||
{isExternal && (
|
||||
<Pill
|
||||
color="warning"
|
||||
className={classes.externalAppLabel}
|
||||
label={intl.formatMessage(messages.externalApp)}
|
||||
data-test-id="app-external-label"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{app.manifestUrl && (
|
||||
<AppManifestTableDisplay manifestUrl={app.manifestUrl} />
|
||||
)}
|
||||
</TableCellAvatar>
|
||||
<TableCell className={clsx(classes.col, classes.colActions)}>
|
||||
<div className={classes.actions}>
|
||||
{app.manifestUrl && isAppInTunnel(app.manifestUrl) ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
className={classes.tunnel}
|
||||
data-test-id="app-tunnel-label"
|
||||
>
|
||||
{`(${intl.formatMessage(messages.tunnelDevelopment)})`}
|
||||
</Typography>
|
||||
) : null}
|
||||
<TableButtonWrapper>
|
||||
<Switch
|
||||
checked={!!app.isActive}
|
||||
onChange={handleToggleActive}
|
||||
data-test-id="app-active-switch"
|
||||
/>
|
||||
</TableButtonWrapper>
|
||||
<AppPermissions permissions={app.permissions} />
|
||||
<TableButtonWrapper>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
color="primary"
|
||||
onClick={() => removeApp(app.id)}
|
||||
data-test-id="app-remove-button"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</TableButtonWrapper>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstalledAppListRow;
|
2
src/new-apps/components/InstalledAppListRow/index.ts
Normal file
2
src/new-apps/components/InstalledAppListRow/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./InstalledAppListRow";
|
||||
export { default } from "./InstalledAppListRow";
|
14
src/new-apps/components/InstalledAppListRow/messages.ts
Normal file
14
src/new-apps/components/InstalledAppListRow/messages.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
externalApp: {
|
||||
id: "nA2g2e",
|
||||
defaultMessage: "External app",
|
||||
description: "label",
|
||||
},
|
||||
tunnelDevelopment: {
|
||||
id: "RjT+Vo",
|
||||
defaultMessage: "Tunnel - development",
|
||||
description: "label",
|
||||
},
|
||||
});
|
55
src/new-apps/components/InstalledAppListRow/styles.ts
Normal file
55
src/new-apps/components/InstalledAppListRow/styles.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
mainContent: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
name: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
version: {
|
||||
color: theme.palette.saleor.main[3],
|
||||
},
|
||||
tunnel: {
|
||||
color: theme.palette.saleor.main[3],
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
actions: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
textAlign: "right",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
externalAppLabel: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
row: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
col: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
colLogo: {
|
||||
width: "100%",
|
||||
"&.MuiTableCell-root:first-child:not(.MuiTableCell-paddingCheckbox)": {
|
||||
paddingLeft: theme.spacing(3),
|
||||
width: "100%",
|
||||
},
|
||||
height: "auto",
|
||||
},
|
||||
colActions: {
|
||||
"&&": {
|
||||
paddingRight: theme.spacing(3),
|
||||
textAlign: "right",
|
||||
},
|
||||
height: "auto",
|
||||
},
|
||||
}),
|
||||
{ name: "InstalledAppListRow" },
|
||||
);
|
|
@ -0,0 +1,42 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { Alert } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import msgs from "./messages";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface MarketplaceAlertProps {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export const MarketplaceAlert: React.FC<MarketplaceAlertProps> = ({
|
||||
error,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const intl = useIntl();
|
||||
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
variant="warning"
|
||||
className={classes.alert}
|
||||
close={true}
|
||||
title={intl.formatMessage(msgs.marketplaceError)}
|
||||
>
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
{...msgs.marketplaceErrorMessage}
|
||||
values={{
|
||||
message: <b>{error.message}</b>,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
MarketplaceAlert.displayName = "MarketplaceAlert";
|
||||
export default MarketplaceAlert;
|
2
src/new-apps/components/MarketplaceAlert/index.ts
Normal file
2
src/new-apps/components/MarketplaceAlert/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { MarketplaceAlert } from "./MarketplaceAlert";
|
||||
export { default } from "./MarketplaceAlert";
|
14
src/new-apps/components/MarketplaceAlert/messages.ts
Normal file
14
src/new-apps/components/MarketplaceAlert/messages.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export default defineMessages({
|
||||
marketplaceError: {
|
||||
id: "cMov2n",
|
||||
defaultMessage: "Error loading apps marketplace",
|
||||
description: "error message",
|
||||
},
|
||||
marketplaceErrorMessage: {
|
||||
id: "F2cR19",
|
||||
defaultMessage: "Error details: {message}",
|
||||
description: "error message",
|
||||
},
|
||||
});
|
10
src/new-apps/components/MarketplaceAlert/styles.ts
Normal file
10
src/new-apps/components/MarketplaceAlert/styles.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
alert: {
|
||||
margin: theme.spacing(6, 0, 12, 0),
|
||||
},
|
||||
}),
|
||||
{ name: "MarketplaceAlert" },
|
||||
);
|
20
src/new-apps/components/SectionHeader/SectionHeader.tsx
Normal file
20
src/new-apps/components/SectionHeader/SectionHeader.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import React from "react";
|
||||
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface SectionHeaderProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const SectionHeader: React.FC<SectionHeaderProps> = ({ title }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Typography variant="h3" className={classes.sectionHeader}>
|
||||
{title}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
SectionHeader.displayName = "SectionHeader";
|
||||
export default SectionHeader;
|
2
src/new-apps/components/SectionHeader/index.ts
Normal file
2
src/new-apps/components/SectionHeader/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./SectionHeader";
|
||||
export { default } from "./SectionHeader";
|
14
src/new-apps/components/SectionHeader/styles.ts
Normal file
14
src/new-apps/components/SectionHeader/styles.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
theme => ({
|
||||
sectionHeader: {
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: theme.palette.saleor.main[3],
|
||||
margin: theme.spacing(8, 0, 3, 0),
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
}),
|
||||
{ name: "AppListPage" },
|
||||
);
|
22
src/new-apps/context.ts
Normal file
22
src/new-apps/context.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import React from "react";
|
||||
|
||||
export interface AppListContextValues {
|
||||
activateApp: (appId: string) => void;
|
||||
deactivateApp: (appId: string) => void;
|
||||
removeApp: (appId: string) => void;
|
||||
}
|
||||
|
||||
export const AppListContext = React.createContext<
|
||||
AppListContextValues | undefined
|
||||
>(undefined);
|
||||
|
||||
export const useAppListContext = () => {
|
||||
const context = React.useContext(AppListContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useAppListContext must be used within a AppListContext");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
198
src/new-apps/fixtures.ts
Normal file
198
src/new-apps/fixtures.ts
Normal file
|
@ -0,0 +1,198 @@
|
|||
import {
|
||||
AppListItemFragment,
|
||||
AppManifestFragment,
|
||||
AppsInstallationsQuery,
|
||||
AppTypeEnum,
|
||||
JobStatusEnum,
|
||||
PermissionEnum,
|
||||
} from "@saleor/graphql";
|
||||
|
||||
import { GetV2SaleorAppsResponse } from "./marketplace.types";
|
||||
|
||||
export const activeApp: AppListItemFragment = {
|
||||
__typename: "App",
|
||||
id: "QXBwOjE3Ng==",
|
||||
isActive: true,
|
||||
name: "First App",
|
||||
type: AppTypeEnum.THIRDPARTY,
|
||||
version: "1.0.0",
|
||||
appUrl: "http://localhost:3000",
|
||||
manifestUrl: "http://localhost:3000/api/manifest",
|
||||
permissions: [
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_USERS,
|
||||
name: "Manage customers.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const inactiveApp: AppListItemFragment = {
|
||||
__typename: "App",
|
||||
id: "QXBwOj4TMb==",
|
||||
isActive: false,
|
||||
name: "Second App",
|
||||
type: AppTypeEnum.THIRDPARTY,
|
||||
version: "1.0.0",
|
||||
appUrl: null,
|
||||
manifestUrl: "http://localhost:3000/api/manifest",
|
||||
permissions: [
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_ORDERS,
|
||||
name: "Manage orders.",
|
||||
},
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_USERS,
|
||||
name: "Manage customers.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const installedAppsList: AppListItemFragment[] = [
|
||||
activeApp,
|
||||
inactiveApp,
|
||||
];
|
||||
|
||||
export const appsInProgress: AppsInstallationsQuery["appsInstallations"] = [
|
||||
{
|
||||
__typename: "AppInstallation",
|
||||
appName: "app",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Failed to connect to app. Try later or contact with app support.",
|
||||
status: JobStatusEnum.FAILED,
|
||||
},
|
||||
{
|
||||
__typename: "AppInstallation",
|
||||
appName: "app pending",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Pending.",
|
||||
status: JobStatusEnum.PENDING,
|
||||
},
|
||||
{
|
||||
__typename: "AppInstallation",
|
||||
appName: "app success",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Success.",
|
||||
status: JobStatusEnum.SUCCESS,
|
||||
},
|
||||
];
|
||||
|
||||
export const installApp: AppManifestFragment = {
|
||||
__typename: "Manifest",
|
||||
about: "Lorem ipsum",
|
||||
appUrl: null,
|
||||
configurationUrl: null,
|
||||
dataPrivacy: null,
|
||||
dataPrivacyUrl: null,
|
||||
homepageUrl: null,
|
||||
identifier: "app",
|
||||
name: "app",
|
||||
permissions: [
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_USERS,
|
||||
name: "Manage users",
|
||||
},
|
||||
{
|
||||
__typename: "Permission",
|
||||
code: PermissionEnum.MANAGE_ORDERS,
|
||||
name: "Manage orders",
|
||||
},
|
||||
],
|
||||
supportUrl: null,
|
||||
tokenTargetUrl: null,
|
||||
version: "1.0",
|
||||
};
|
||||
|
||||
export const releasedApp: GetV2SaleorAppsResponse.ReleasedSaleorApp = {
|
||||
name: {
|
||||
en: "Test released app",
|
||||
},
|
||||
description: {
|
||||
en: "Test released app description",
|
||||
},
|
||||
logo: {
|
||||
source: "https://www.released-example.com/images/logo.png",
|
||||
color: "#000000",
|
||||
},
|
||||
manifestUrl: "https://www.released-example.com/manifest",
|
||||
privacyUrl: "https://www.released-example.com/privacy",
|
||||
supportUrl: "https://www.released-example.com/support",
|
||||
repositoryUrl: "https://www.released-example.com/repository",
|
||||
vercelDeploymentUrl: "https://www.released-example.com/deployment",
|
||||
integrations: [
|
||||
{
|
||||
name: "First released integration",
|
||||
logo: {
|
||||
light: {
|
||||
source:
|
||||
"https://www.released-example.com/images/first-integration-logo-light.png",
|
||||
},
|
||||
dark: {
|
||||
source:
|
||||
"https://www.released-example.com/images/first-integration-logo-dark.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Second released integration",
|
||||
logo: {
|
||||
light: {
|
||||
source:
|
||||
"https://www.released-example.com/images/second-integration-logo-light.png",
|
||||
},
|
||||
dark: {
|
||||
source:
|
||||
"https://www.released-example.com/images/second-integration-logo-dark.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const comingSoonApp: GetV2SaleorAppsResponse.ComingSoonSaleorApp = {
|
||||
name: {
|
||||
en: "Test coming soon app",
|
||||
},
|
||||
description: {
|
||||
en: "Test coming soon app description",
|
||||
},
|
||||
logo: {
|
||||
source: "https://www.coming-soon-example.com/images/logo.png",
|
||||
color: "#000000",
|
||||
},
|
||||
releaseDate: "2019-12-16",
|
||||
integrations: [
|
||||
{
|
||||
name: "First coming soon integration",
|
||||
logo: {
|
||||
light: {
|
||||
source:
|
||||
"https://www.coming-soon-example.com/images/first-integration-logo-light.png",
|
||||
},
|
||||
dark: {
|
||||
source:
|
||||
"https://www.coming-soon-example.com/images/first-integration-logo-dark.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Second coming soon integration",
|
||||
logo: {
|
||||
light: {
|
||||
source:
|
||||
"https://www.coming-soon-example.com/images/second-integration-logo-light.png",
|
||||
},
|
||||
dark: {
|
||||
source:
|
||||
"https://www.coming-soon-example.com/images/second-integration-logo-dark.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
66
src/new-apps/hooks/useMarketplaceApps.test.ts
Normal file
66
src/new-apps/hooks/useMarketplaceApps.test.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { comingSoonApp, releasedApp } from "../fixtures";
|
||||
import useMarketplaceApps from "./useMarketplaceApps";
|
||||
|
||||
const mockApps = [releasedApp, comingSoonApp];
|
||||
|
||||
global.fetch = jest.fn(url => {
|
||||
if (url === "https://marketplace.com/apps") {
|
||||
return Promise.resolve(({
|
||||
ok: true,
|
||||
json: jest.fn(() => Promise.resolve(mockApps)),
|
||||
} as unknown) as Response);
|
||||
}
|
||||
if (url === "https://marketplace.com/failing-apps-endpoint") {
|
||||
return Promise.resolve(({
|
||||
ok: false,
|
||||
statusText: "API error",
|
||||
} as unknown) as Response);
|
||||
}
|
||||
return Promise.reject(new Error("API is down"));
|
||||
});
|
||||
|
||||
describe("apps hooks useMarketplaceApps", () => {
|
||||
it("should return apps when request to proper marketplace url returns apps", async () => {
|
||||
// Arrange
|
||||
const marketplaceUrl = "https://marketplace.com/apps";
|
||||
|
||||
// Act
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useMarketplaceApps(marketplaceUrl),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({ data: mockApps });
|
||||
});
|
||||
|
||||
it("should return error when request to proper marketplace url returns error", async () => {
|
||||
// Arrange
|
||||
const marketplaceUrl = "https://marketplace.com/failing-apps-endpoint";
|
||||
|
||||
// Act
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useMarketplaceApps(marketplaceUrl),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({ error: Error("API error") });
|
||||
});
|
||||
|
||||
it("should return error when request to wrong marketplace url fails", async () => {
|
||||
// Arrange
|
||||
const marketplaceUrl = "https://wrong-marketplace.com";
|
||||
|
||||
// Act
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useMarketplaceApps(marketplaceUrl),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({ error: Error("API is down") });
|
||||
});
|
||||
});
|
107
src/new-apps/hooks/useMarketplaceApps.ts
Normal file
107
src/new-apps/hooks/useMarketplaceApps.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { GetV2SaleorAppsResponse } from "@saleor/new-apps/marketplace.types";
|
||||
import { useEffect, useReducer, useRef } from "react";
|
||||
|
||||
interface State {
|
||||
data?: GetV2SaleorAppsResponse.SaleorApp[];
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
interface Cache {
|
||||
[url: string]: GetV2SaleorAppsResponse.SaleorApp[];
|
||||
}
|
||||
|
||||
// discriminated union type
|
||||
type Action =
|
||||
| { type: "loading" }
|
||||
| { type: "fetched"; payload: GetV2SaleorAppsResponse.SaleorApp[] }
|
||||
| { type: "error"; payload: Error };
|
||||
|
||||
/**
|
||||
* Hook used to fetch apps list available under given marketplace url.
|
||||
* @param marketplaceUrl - url from which fetch data with apps list
|
||||
* @returns state object containing data with apps list or fetch error
|
||||
*/
|
||||
function useMarketplaceApps(marketplaceUrl?: string): State {
|
||||
const cache = useRef<Cache>({});
|
||||
|
||||
// Used to prevent state update if the component is unmounted
|
||||
const cancelRequest = useRef<boolean>(false);
|
||||
|
||||
const initialState: State = {
|
||||
error: undefined,
|
||||
data: undefined,
|
||||
};
|
||||
|
||||
// Keep state logic separated
|
||||
const fetchReducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "loading":
|
||||
return { ...initialState };
|
||||
case "fetched":
|
||||
return {
|
||||
...initialState,
|
||||
data: action.payload,
|
||||
error: undefined,
|
||||
};
|
||||
case "error":
|
||||
return { ...initialState, error: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
// Do nothing if the url is not given
|
||||
if (!marketplaceUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelRequest.current = false;
|
||||
|
||||
const fetchData = async () => {
|
||||
dispatch({ type: "loading" });
|
||||
|
||||
// If a cache exists for this url, return it
|
||||
if (cache.current[marketplaceUrl]) {
|
||||
dispatch({ type: "fetched", payload: cache.current[marketplaceUrl] });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(marketplaceUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as GetV2SaleorAppsResponse.SaleorApp[];
|
||||
cache.current[marketplaceUrl] = data;
|
||||
if (cancelRequest.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: "fetched", payload: data });
|
||||
} catch (error) {
|
||||
if (cancelRequest.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: "error", payload: error as Error });
|
||||
}
|
||||
};
|
||||
|
||||
void fetchData();
|
||||
|
||||
// Use the cleanup function for avoiding a possibly...
|
||||
// ...state update after the component was unmounted
|
||||
return () => {
|
||||
cancelRequest.current = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [marketplaceUrl]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default useMarketplaceApps;
|
31
src/new-apps/index.tsx
Normal file
31
src/new-apps/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { sectionNames } from "@saleor/intl";
|
||||
import { parse as parseQs } from "qs";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Route, RouteComponentProps, Switch } from "react-router-dom";
|
||||
|
||||
import { WindowTitle } from "../components/WindowTitle";
|
||||
import { AppListUrlQueryParams, AppPaths } from "./urls";
|
||||
import AppListView from "./views/AppList";
|
||||
|
||||
const AppList: React.FC<RouteComponentProps> = () => {
|
||||
const qs = parseQs(location.search.substr(1));
|
||||
const params: AppListUrlQueryParams = qs;
|
||||
|
||||
return <AppListView params={params} />;
|
||||
};
|
||||
|
||||
const Apps = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<>
|
||||
<WindowTitle title={intl.formatMessage(sectionNames.apps)} />
|
||||
<Switch>
|
||||
<Route exact path={AppPaths.appListPath} component={AppList} />
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Apps;
|
40
src/new-apps/marketplace.types.ts
Normal file
40
src/new-apps/marketplace.types.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace GetV2SaleorAppsResponse {
|
||||
export interface SaleorAppBase {
|
||||
name: {
|
||||
en: string;
|
||||
};
|
||||
description: {
|
||||
en: string;
|
||||
};
|
||||
logo: {
|
||||
source: string | null;
|
||||
color: string;
|
||||
};
|
||||
integrations: Array<{
|
||||
name: string;
|
||||
logo: {
|
||||
light: {
|
||||
source: string;
|
||||
};
|
||||
dark: {
|
||||
source: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export type ReleasedSaleorApp = SaleorAppBase & {
|
||||
repositoryUrl: string;
|
||||
supportUrl: string;
|
||||
privacyUrl: string;
|
||||
manifestUrl: string;
|
||||
vercelDeploymentUrl?: string;
|
||||
};
|
||||
|
||||
export type ComingSoonSaleorApp = SaleorAppBase & {
|
||||
releaseDate: string;
|
||||
};
|
||||
|
||||
export type SaleorApp = ReleasedSaleorApp | ComingSoonSaleorApp;
|
||||
}
|
24
src/new-apps/messages.ts
Normal file
24
src/new-apps/messages.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const appsMessages = defineMessages({
|
||||
repository: {
|
||||
id: "f91E8b",
|
||||
defaultMessage: "Repository",
|
||||
description: "app repository",
|
||||
},
|
||||
support: {
|
||||
id: "/eTYOp",
|
||||
defaultMessage: "Support",
|
||||
description: "app support",
|
||||
},
|
||||
dataPrivacy: {
|
||||
id: "GKQGKG",
|
||||
defaultMessage: "Data privacy",
|
||||
description: "app data privacy",
|
||||
},
|
||||
appManifestUrl: {
|
||||
id: "4IgzXs",
|
||||
defaultMessage: "App Manifest URL",
|
||||
description: "label",
|
||||
},
|
||||
});
|
6
src/new-apps/tsconfig.json
Normal file
6
src/new-apps/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue