Compare commits

...

77 commits

Author SHA1 Message Date
4b12982597 add changeset 2023-09-30 22:22:32 +02:00
292a5bdb0c emails-and-messages redis apl support 2023-09-30 22:03:02 +02:00
Lukasz Ostrowski
653b98df86
Release apps (#1041)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-28 16:02:35 +02:00
Krzysztof Wolski
ae6dbb125b
Remove cache update on product webhooks (#1036)
* Product feed - change max function exec time to 5 minutes

* Remove cache update on product webhooks

* Add webhooks migration

---------

Co-authored-by: Lukasz Ostrowski <lukasz.ostrowski@saleor.io>
2023-09-27 12:56:12 +02:00
Lukasz Ostrowski
6948fe41ca
Update CODEOWNERS 2023-09-23 10:49:49 +02:00
Lukasz Ostrowski
2a22cf47f2
🚀 Release apps (#988)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-18 10:20:01 +02:00
Przemysław Łada
927d2b3bb6
Use full commit hash when referencing GitHub Action (#1002)
* Use full commit hash when referencing GitHub Action

* Use full commit hash in changeset-checker
2023-09-18 09:28:24 +02:00
dependabot[bot]
5dee65ad2c
[skip ci]: Bump the codegen group with 1 update (#990)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
2023-09-13 11:04:10 +02:00
Przemysław Łada
c6db32b40e
Fix broken pnpm lock (#1009) 2023-09-08 20:38:48 +02:00
dependabot[bot]
5d3d81d5c2
[skip ci]: Bump @hookform/resolvers from 2.9.11 to 3.3.1 (#993)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lukasz Ostrowski <lukasz.ostrowski@saleor.io>
2023-09-08 15:35:57 +02:00
Przemysław Łada
52c46987f3
Replace assign-pr-creator-action with GitHub CLI (#1008) 2023-09-08 15:35:32 +02:00
Lukasz Ostrowski
1982d81f17
Extend pricing data in Algolia (#998)
Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
2023-09-08 14:05:37 +02:00
Krzysztof Wolski
767d0be722
Add mandatory check for the changeset (#1000)
* Add mandatory check for the changeset

* Skip check on label

* Fx typo and add synchronize event
2023-09-08 14:04:34 +02:00
dependabot[bot]
2951fb3ab6
[skip ci]: Bump jotai from 2.0.0 to 2.4.2 (#1007)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lukasz Ostrowski <lukasz.ostrowski@saleor.io>
2023-09-08 10:40:34 +00:00
Krzysztof Wolski
7e0755ec9e
Webhook helpers (#949)
* WIP

* Added script and making implementation more roboust

* Added rollback on issues with the migration

* Cleanup the code

* Use allSettled instead of all

* Do not check spelling in schema files.
Schema is pulled from the API and is not controlled by our team

* Update the pkg json

* Fix typo on log message

* Add dedupe to the ignored words.
Its used by codegen

* Add changesets
2023-09-07 13:04:23 +02:00
Adrian Pilarczyk
a32fe7caf4
feat: taxes client logs (#944)
* feat: 🚧 add skeleton logs page

* feat:  add ClientLogsMetadataRepository

* feat:  add AvataxClientLogger and router

* feat:  implement avatax client logs in order confirmed

* feat: 🚧 add skeleton ui for logs

* feat:  add avatax client logger

* refactor: ♻️ move breadcrumbs to page lvl

* feat:  logger per config

* feat:  add logs to remaining events

* refactor: ♻️ pass clientLogger from webhook service

* feat:  add taxjar logger

* test:  add tests for logs push

* feat:  add getAll test

* feat:  add missing taxjar page

* refactor: ♻️ move unshiftItemToLimitedArray to log-utils and test

* fix: 🐛 label

* refactor: 💄 styles

* build: 👷 changeset

* fix: 🐛 typo

* refactor: ♻️ address feedback on backend side

* refactor: ♻️ adjust UI to merging of avatax & taxjar logs

* feat:  make client logs table box scrollable

* fix: 🐛 size of container

* fix: 🐛 loading on isRefetching, not isFetching

* fix: 🐛 no lines early error
2023-09-07 10:42:54 +02:00
dependabot[bot]
4a2275e999
[skip ci]: Bump the trpc group with 4 updates (#987)
Bumps the trpc group with 4 updates: [@trpc/client](https://github.com/trpc/trpc/tree/HEAD/packages/client), [@trpc/next](https://github.com/trpc/trpc/tree/HEAD/packages/next), [@trpc/react-query](https://github.com/trpc/trpc/tree/HEAD/packages/react) and [@trpc/server](https://github.com/trpc/trpc/tree/HEAD/packages/server).


Updates `@trpc/client` from 10.34.0 to 10.38.1
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v10.38.1/packages/client)

Updates `@trpc/next` from 10.34.0 to 10.38.1
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v10.38.1/packages/next)

Updates `@trpc/react-query` from 10.34.0 to 10.38.1
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v10.38.1/packages/react)

Updates `@trpc/server` from 10.34.0 to 10.38.1
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v10.38.1/packages/server)

---
updated-dependencies:
- dependency-name: "@trpc/client"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: trpc
- dependency-name: "@trpc/next"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: trpc
- dependency-name: "@trpc/react-query"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: trpc
- dependency-name: "@trpc/server"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: trpc
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 15:24:18 +02:00
dependabot[bot]
a5f226bdef
[skip ci]: Bump the rtl group with 1 update (#991)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 14:49:20 +02:00
Lukasz Ostrowski
a0a607ce99
Update app-sdk to 0.43.1 (#956) 2023-09-06 12:26:04 +00:00
Lukasz Ostrowski
6ac7799d72
Update dependabot.yaml 2023-09-06 14:23:31 +02:00
Lukasz Ostrowski
4aee4e11f8
Invoices, Klaviyo: Refactor to shared components (#989) 2023-09-06 12:22:54 +00:00
Lukasz Ostrowski
86bc946b3e
Update README.md (#962) 2023-09-06 14:22:40 +02:00
Lukasz Ostrowski
1033b93747
Update pnpm version expected by the monorepo (#985) 2023-09-06 10:50:07 +02:00
Lukasz Ostrowski
2e296996cb
Update sentry and add Sentry CLI to the workspace (#986) 2023-09-06 10:24:45 +02:00
Lukasz Ostrowski
068e529cfe
Update dependabot.yaml 2023-09-06 09:59:31 +02:00
dependabot[bot]
3bfcfa5b01
[skip ci]: Bump prettier from 3.0.1 to 3.0.3 (#983)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 09:47:49 +02:00
Lukasz Ostrowski
6688a55102
Update PULL_REQUEST_TEMPLATE.md 2023-09-05 19:00:46 +02:00
dependabot[bot]
4efebde36d
[skip ci]: Bump eslint-plugin-react from 7.32.2 to 7.33.2 (#979)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-05 11:47:00 +02:00
Lukasz Ostrowski
9f76a2f1f8
Update dependabot.yaml 2023-09-05 09:47:19 +02:00
Lukasz Ostrowski
627712bef3
Update dependabot.yaml 2023-09-05 09:12:35 +02:00
Lukasz Ostrowski
8e4941fd2a
🚀 Release apps (#942)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-05 08:58:31 +02:00
Lukasz Ostrowski
55315b89f3
Fix Search webhooks toggling (#961)
* Fix Search webhooks toggling

* fix test
2023-09-04 20:13:58 +02:00
Lukasz Ostrowski
30140eea06
Improved some spellings and add some words to allow list (#959) 2023-09-04 11:53:44 +02:00
Lukasz Ostrowski
e8660e8bb9
Extract shared packages (#948)
* [skip ci] tRPC shared package

* [skip ci] tRPC shared package - fix

* [skip ci] shared package - app sections

* [skip ci] segment - implement shared components

* [skip ci] extract theme synchronizer

* extract components and implement them in apps

* cms - extract shared packages

* Fix imports

* remove urql from peer deps
2023-09-04 11:30:57 +02:00
Adrian Pilarczyk
ceddcf96eb
refactor: ♻️ omit more in cspell config (#957) 2023-09-04 11:21:28 +02:00
Lukasz Ostrowski
1e3c08c029
Algolia fields filtering (#946)
* wip

* crud for algolia fields settings

* add ui form fields confiugraion

* adjust app to new config

* filter mapping with fields

* fix lang

* fix lang
2023-09-01 17:01:41 +02:00
Krzysztof Wolski
23e71bc7a2
Fix save button label (#950) 2023-09-01 14:08:10 +02:00
Krzysztof Wolski
261e9d1a0c
Product Feed: Add configurable image size and additional photos (#915)
* Add configurable image size and additional photos

* CR fixes

* CSpell fixes
2023-09-01 13:35:02 +02:00
Adrian Pilarczyk
0aa1d12cc4
feat: ⚗️ add check-spelling.yml gh action (#947)
* feat: ⚗️ add check-spelling.yml gh action

* chore: ⚗️ make typos to test gh action

* feat:  add cspell config

* fix: 🐛 typos

* feat: ⚗️ test verbose

* Revert "feat: ⚗️ test verbose"

This reverts commit 3bf36f5a29b6af2ca969a92f0bc61e12e89d8a44.

* feat:  add cspell locally

scripts for all files & lint-staged

* feat:  add new words to dictionary in cspell.json

* refactor: 🚚 cspell.json -> cspell.config.json

* Revert "refactor: 🚚 cspell.json -> cspell.config.json"

This reverts commit b5c96f6909cf5f0ab1173255ef7ded7e360ca83f.

* Revert "feat:  add new words to dictionary in cspell.json"

This reverts commit 69d8bdd3f9ed19ab07044e3e986c8d0b06114e43.

* feat:  add ignorePaths

* feat: add more ignore

* feat:  add words to cspell.json

* refactor: ♻️ change dictionary in cspell.json

* refactor: make words capitalized

* fix: 🐛 add md/mdx to check-spelling
2023-09-01 11:44:20 +02:00
Lukasz Ostrowski
ed30a818e3
Refactors: webhooks, config (#941) 2023-08-30 12:17:44 +02:00
Lukasz Ostrowski
056209486c
Release apps (#934)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-30 10:43:22 +02:00
Lukasz Ostrowski
0f84985c98
Add tRPC to search app (#940)
* Add tRPC to search app

* Implemented configuration endpoint in trpc

* replace settings manager to use shared factory

* replace configuration calls to trpc
2023-08-29 22:53:51 +02:00
Krzysztof Wolski
2a1385bab1
Search: Fix stale variants after product removal, add indices setup and product descriptions (#932)
* Move plaintext renderer to shared package and use it for Algolia documents

* Fix stale variants after product removal, add indices setup

* Search: webhooks migration script (#936)

* Add webhook recreation script

* Add changeset
2023-08-29 13:19:31 +02:00
Lukasz Ostrowski
a81f061fcf
update macaw (#925) 2023-08-29 10:47:50 +02:00
Lukasz Ostrowski
d9d0f64a01
Fix CMS modal bg (#935) 2023-08-28 21:51:15 +02:00
Krzysztof Wolski
fcc37e7c7e
Remove clsx package which is no longer used (#899)
* Remove clsx package which is no longer used

* Add changeset

* Remove clsx from data importer

* Remove unused clsx package
2023-08-28 19:23:11 +02:00
Adrian Pilarczyk
254cd4c9a3
Fix typo in Segment (#933)
* Fix typo

* build: 👷 add changeset
2023-08-28 13:41:54 +02:00
Lukasz Ostrowski
17925f2115
Release apps (#917)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-28 08:19:32 +02:00
Lukasz Ostrowski
e9378e7af7
fix payloadcms fields (#924)
Co-authored-by: Adrian Pilarczyk <admin@peelar.dev>
2023-08-25 15:33:18 +02:00
Adrian Pilarczyk
34efd39dcf
fix customer code calculate taxes (#922)
* fix: 🐛 calculate taxes customerCode

* build: 👷 changeset

* refactor: ♻️ address feedback

* feat:  add migration

* Empty-Commit
2023-08-25 14:50:47 +02:00
Lukasz Ostrowski
6f1c5c9436
Add Payload CMS (#905) 2023-08-25 11:34:28 +02:00
Adrian Pilarczyk
45ed9fb444
fix checkout calculate taxes (#919)
* fix: 🐛 value of customerCode in calculateTaxes

* build: 👷 add changeset

* fix: 🐛 tests
2023-08-25 11:19:53 +02:00
Adrian Pilarczyk
4a635620c4
feat: improve migration scripts visibility (#918)
* feat:  add emojis

* refactor: 🚚 migration file
2023-08-24 11:56:28 +02:00
Adrian Pilarczyk
783bd5ec55
Complete migration to OrderConfirmed flow (#916)
* refactor: 🔥 order-created & order-fulfilled code

* build: 👷 add changeset

* fix: 🐛 fix build
2023-08-24 10:32:31 +02:00
Lukasz Ostrowski
3bd2d33d97
🚀 Release apps (#897)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-24 08:28:40 +02:00
Lukasz Ostrowski
6ea3e38bd8
Changed events mapping in Segment (#914) 2023-08-23 13:42:38 +02:00
Adrian Pilarczyk
be761b251e
chore: split credentials and settings (#886)
* feat:  grey out disabled links

* chore: 🚚 move fields to new avatax settings fragment

* build: 👷 add changeset

* refactor: 🚚 move companyCode to credentials

* refactor: ♻️ make helper texts more accurate

* refactor: 🚚 Avatax -> AvaTax
2023-08-21 10:28:43 +02:00
Lukasz Ostrowski
c50797e836
add metadata manager to shared package (#901) 2023-08-17 11:53:03 +02:00
Lukasz Ostrowski
8b3d9617e6
update macaw to pre-118 (#898) 2023-08-17 09:00:47 +02:00
Lukasz Ostrowski
a2cac7849b Fix segment changeset 2023-08-16 16:31:53 +02:00
Lukasz Ostrowski
895004dd55
Release apps (#885)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-16 15:31:23 +02:00
Lukasz Ostrowski
7d574c70c9
Introduce Twilio Segment App (#881)
* bootstrap segment app from cms app

* tracking logic

* schema configuratioj

* config form

* form saving

* Connected order_created

* add more fields

* Order updated webhook

* Order cancelled event

* order refunded webhook

* order fully paid

* update deps

* error handling

* logger

* logs

* Add app to workflow

* add icon

* remove breadcrumbs

* Change 400 to 200 response if payload is invalid

* link to docs

* change semgent.io to segment
2023-08-16 15:30:46 +02:00
Krzysztof Wolski
69fe973121
Search: Add media and stock flag (#874)
* Add media and stock flag

* Add UI for updating the queries

* Add update info to changelog
2023-08-16 14:08:07 +02:00
Krzysztof Wolski
3002354c5e
Improve logging on tRPC routes (#884) 2023-08-10 19:09:06 +02:00
Lukasz Ostrowski
7f0d8d24a3
Release apps (#883)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-10 17:49:06 +02:00
Krzysztof Wolski
bb504d95aa
Product feed: Fix title template form (#882)
* Improve logging

* Fix error on parsing metadata created in the previous version

* Fix title template form

* Add changesets

* Use info level instead of debug
2023-08-10 17:37:59 +02:00
Lukasz Ostrowski
7b19ab44c3
🚀 Release apps (#865)
* Release apps

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Adrian Pilarczyk <admin@peelar.dev>
2023-08-10 14:03:02 +02:00
Adrian Pilarczyk
416c92fb6c
feat: change the flow from OrderCreated to OrderConfirmed (#826)
* refactor: ♻️ extract order-metadata-manager

* feat: 🚧 add basic boilerplate

* feat:  add readExternalIdFromOrderMetadata

* Revert "feat:  add readExternalIdFromOrderMetadata"

This reverts commit a78d9d4597672f8605cf998a9f784aebaab27de1.

* feat:  add order-cancelled avatax adapter

* test:  add tests for AvataxOrderCancelledPayloadTransformer

* refactor: avataxId instead of externalId

* refactor: ♻️ split up webhook response

* build: ⬆️ upgrade avatax

* refactor: ♻️ extend logging in webhook response errors

* fix: 🐛 split privateMetadata and publicMetadata

* fix: 🐛 use "DEFAULT" value of companyCode for commit to work

* fix: ⚗️ fix voidTransaction type

* refactor: 🚚 order_created -> order_confirmed

* fix: 🐛 change voidReason

* build: 👷 add changeset

* refactor: 🔥 order_fulfilled webhook

* feat: Avatax metadata tax calculation date (#843)

* feat:  add metadata tax calculation date

* build: 👷 add changeset

* feat: Avatax metadata document code (#844)

* feat:  provide document code through metadata field

* build: 👷 add changeset

* refactor: ♻️ fallback to default company code for migration

* refactor: ♻️ patch order-created files and add deprecation note

* Revert "refactor: 🔥 order_fulfilled webhook"

This reverts commit fd098642735ae9d62e3a876088226bd0f108afd6.

* refactor: ♻️ patch order-fulfilled files and add deprecation note

* fix: 🐛 bring back deprecated webhooks to manifest

* feat: ⚗️ add AppWebhookMigrator (#850)

* refactor: 🚚 order_created -> order_confirmed

* refactor: 🔥 order_fulfilled webhook

* feat: ⚗️ add AppWebhookMigrator

* feat:  add mode to migrator

* feat:  add draft of run-report and migrateWebhook method

* refactor: ♻️ address feedback

* feat:  add tests and new structure

* refactor: 🔥 util

* feat:  add enable/disable webhook rollback flow

* refactor: ♻️ modify the taxes-migration flow

* refactor: ♻️ generalize document code & date resolver

* chore: 🗃️ add run-migration

* chore: 💡 update comments about migration flow

* fix: 🐛 slice document code

* refactor: ♻️ try/catch at the top level

* chore: 💡 add comments

* Update shiny-meals-wait.md

* Update soft-steaks-know.md

* Update soft-steaks-know.md

* fix:  fix test

* feat:  change createTransaction to createOrAdjustTransaction

this feature grants idempotency of the transaction flow

* feat:  add number field to OrderConfirmed payload

* chore: 💡 add deprecation comment to metadata method

* docs: 📝 add todo comment to refactor sumPayloadLines

* feat:  add resolveStringOrThrow and use it for email

* fix: 🐛 add missing number to mock
2023-08-10 13:08:20 +02:00
Lukasz Ostrowski
c7b3d35f5d
Update README.md 2023-08-09 12:23:31 +02:00
Krzysztof Wolski
b8c528397a
Use a proper capitalization of the SendGrid name (#869) 2023-08-08 14:18:11 +02:00
Lukasz Ostrowski
fe767a494b
CMS - change manifest name (#862) 2023-08-08 09:33:04 +02:00
Lukasz Ostrowski
a951af938e
Release apps (#842)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-07 17:02:13 +02:00
Krzysztof Wolski
6a84b4b7e0
Product feed: Use Handlebars for title and URL formatting (#859)
* Use handlebars templates for title and URLs

* Add changesets

* Allow chaining on set methods

* Fix failing test

* Use getValues instead of handle submit

* Narrow down type for data input
2023-08-07 16:33:01 +02:00
Krzysztof Wolski
0b0297eeb8
Product feed: Product Pricing update (#840)
* Add attribute mapping

* Improve release note

* Log the error

* Add pattern attribute

* Add group ID

* Update the item pricing
2023-08-04 13:59:40 +02:00
Lukasz Ostrowski
261957fda4
Update dev dependencies (#845)
* update prettier and syncpack

* update turbo

* Update various dev deps

* improve syncpack config

* remove c8 reporter from invoices

* fix test packages

* Fix invocies test
2023-08-04 08:39:44 +02:00
Krzysztof Wolski
fc5e6396d2
Product Feed: Add group ID (#839)
* Add attribute mapping

* Improve release note

* Log the error

* Add pattern attribute

* Add group ID
2023-08-03 11:14:56 +02:00
Krzysztof Wolski
aece07338e
Product Feed: Add attribute mapping (#838)
* Add attribute mapping

* Improve release note

* Log the error

* Add pattern attribute
2023-08-03 10:27:14 +02:00
511 changed files with 118082 additions and 6144 deletions

View file

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

View file

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

View file

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

3
.github/labeler.yml vendored
View file

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

View file

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

31
.github/workflows/changeset-checker.yml vendored Normal file
View file

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

View file

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

10
.github/workflows/check-spelling.yml vendored Normal file
View file

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

View file

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

View file

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

3
.gitignore vendored
View file

@ -42,4 +42,5 @@ apps/**/generated
.sentryclirc .sentryclirc
.vscode/ .vscode/
.cspellcache

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,9 +7,7 @@ import { useForm } from "react-hook-form";
import { BuilderIoProviderConfig, SaleorProviderFieldsMappingKeys } from "../../configuration"; import { BuilderIoProviderConfig, SaleorProviderFieldsMappingKeys } from "../../configuration";
import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields"; import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields";
import { trpcClient } from "../../trpc/trpc-client"; import { trpcClient } from "../../trpc/trpc-client";
import { ButtonsBox } from "../../ui/buttons-box"; import { ButtonsBox, SkeletonLayout, TextLink } from "@saleor/apps-ui";
import { TextLink } from "@saleor/apps-ui";
import { Skeleton } from "@/modules/ui/skeleton";
type FormShape = Omit<BuilderIoProviderConfig.InputShape, "type">; type FormShape = Omit<BuilderIoProviderConfig.InputShape, "type">;
const FormSchema = BuilderIoProviderConfig.Schema.Input.omit({ type: true }); const FormSchema = BuilderIoProviderConfig.Schema.Input.omit({ type: true });
@ -104,7 +102,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
padding={2} padding={2}
> >
<Text variant="caption">Saleor Field</Text> <Text variant="caption">Saleor Field</Text>
<Text variant="caption">Contentful field</Text> <Text variant="caption">Builder.io field</Text>
</Box> </Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => ( {SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component // todo extract this table to component
@ -127,7 +125,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
size="small" size="small"
control={control} control={control}
name={`productVariantFieldsMapping.${saleorField}`} name={`productVariantFieldsMapping.${saleorField}`}
label="Builer.io Field" label="Builder.io Field"
/> />
</Box> </Box>
))} ))}
@ -190,7 +188,7 @@ const EditFormVariant = (props: { configId: string }) => {
}, },
{ {
enabled: !!props.configId, enabled: !!props.configId,
} },
); );
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() { onSuccess() {
@ -207,11 +205,11 @@ const EditFormVariant = (props: { configId: string }) => {
}); });
if (!data) { if (!data) {
return <Skeleton.Section />; return <SkeletonLayout.Section />;
} }
if (data.type !== "builder.io") { if (data.type !== "builder.io") {
throw new Error("Trying to fill builer.io form with non builder.io data"); throw new Error("Trying to fill builder.io form with non builder.io data");
} }
return ( return (

View file

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

View file

@ -8,8 +8,8 @@ import React, { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields"; import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields";
import { trpcClient } from "../../trpc/trpc-client"; import { trpcClient } from "../../trpc/trpc-client";
import { ButtonsBox } from "../../ui/buttons-box";
import { DatocmsProviderConfig } from "@/modules/configuration/schemas/datocms-provider.schema"; import { DatocmsProviderConfig } from "@/modules/configuration/schemas/datocms-provider.schema";
import { ButtonsBox } from "@saleor/apps-ui";
type FormShape = Omit<DatocmsProviderConfig.InputShape, "type">; type FormShape = Omit<DatocmsProviderConfig.InputShape, "type">;
@ -43,7 +43,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
notifyError( notifyError(
"Error", "Error",
"Could not fetch content types from DatoCMS. Please check your credentials." "Could not fetch content types from DatoCMS. Please check your credentials.",
); );
}, },
}); });
@ -61,7 +61,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
notifyError( notifyError(
"Error", "Error",
"Could not fetch content types from DatoCMS. Please check your credentials." "Could not fetch content types from DatoCMS. Please check your credentials.",
); );
}, },
}); });
@ -161,7 +161,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
{fieldsData && ( {fieldsData && (
<React.Fragment> <React.Fragment>
<Text as="p" variant="heading" size="small"> <Text as="p" variant="heading" size="small">
Map fields from Saleor to your contentful schema. Map fields from Saleor to your DatoCMS schema.
</Text> </Text>
<Text as="p" marginTop={2} marginBottom={4}> <Text as="p" marginTop={2} marginBottom={4}>
All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should
@ -177,7 +177,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
padding={2} padding={2}
> >
<Text variant="caption">Saleor Field</Text> <Text variant="caption">Saleor Field</Text>
<Text variant="caption">Contentful field</Text> <Text variant="caption">DatoCMS field</Text>
</Box> </Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => ( {SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component // todo extract this table to component
@ -271,7 +271,7 @@ const EditFormVariant = (props: { configId: string }) => {
}, },
{ {
enabled: !!props.configId, enabled: !!props.configId,
} },
); );
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() { onSuccess() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,13 @@
import { httpBatchLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next"; import { createTRPCNext } from "@trpc/next";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const"; import { createHttpBatchLink } from "@saleor/trpc";
import { appBridgeInstance } from "../../pages/_app"; import { appBridgeInstance } from "../../pages/_app";
import { AppRouter } from "./trpc-app-router"; import { AppRouter } from "./trpc-app-router";
function getBaseUrl() {
if (typeof window !== "undefined") return "";
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const trpcClient = createTRPCNext<AppRouter>({ export const trpcClient = createTRPCNext<AppRouter>({
config({ ctx }) { config() {
return { return {
links: [ links: [createHttpBatchLink(appBridgeInstance)],
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers() {
return {
/**
* Attach headers from app to client requests, so tRPC can add them to context
*/
[SALEOR_AUTHORIZATION_BEARER_HEADER]: appBridgeInstance?.getState().token,
[SALEOR_API_URL_HEADER]: appBridgeInstance?.getState().saleorApiUrl,
};
},
}),
],
// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
}; };
}, },

View file

@ -1,6 +1,7 @@
import { initTRPC } from "@trpc/server"; import { initTRPC } from "@trpc/server";
import { TrpcContext } from "./trpc-context";
import { Permission } from "@saleor/app-sdk/types"; import { Permission } from "@saleor/app-sdk/types";
import { TrpcContext } from "@saleor/trpc";
interface Meta { interface Meta {
requiredClientPermissions?: Permission[]; requiredClientPermissions?: Permission[];

View file

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

View file

@ -6,12 +6,11 @@ import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import React, { ReactElement } from "react"; import React, { ReactElement } from "react";
import { AppProps } from "next/app"; import { AppProps } from "next/app";
import { NoSSRWrapper } from "@saleor/apps-shared"; import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared";
import { trpcClient } from "../modules/trpc/trpc-client"; import { trpcClient } from "../modules/trpc/trpc-client";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next"; import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NextPage } from "next"; import { NextPage } from "next";
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
/** /**
* Ensure instance is a singleton. * Ensure instance is a singleton.

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,8 +1,3 @@
body {
font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
a { a {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;

View file

@ -8,9 +8,5 @@ export default defineConfig({
environment: "jsdom", environment: "jsdom",
setupFiles: "./src/setup-tests.ts", setupFiles: "./src/setup-tests.ts",
css: false, css: false,
coverage: {
provider: "c8",
reporter: ["text-summary", "cobertura"],
},
}, },
}); });

View file

@ -1,5 +1,57 @@
# saleor-app-klaviyo # saleor-app-klaviyo
## 1.9.0
### Minor Changes
- 4aee4e1: Improved app layout to match modern style.
### Patch Changes
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
- 4aee4e1: Fixed error where config couldn't be saved
## 1.8.6
### Patch Changes
- 30140ee: Improved some text typos.
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- @saleor/apps-shared@1.9.0
## 1.8.5
### Patch Changes
- a81f061: Updated Macaw to pre-127
- fcc37e7: Remove clsx package from the projects no longer using it.
- Updated dependencies [2a1385b]
- Updated dependencies [a81f061]
- Updated dependencies [fcc37e7]
- @saleor/apps-shared@1.8.1
## 1.8.4
### Patch Changes
- 8b3d961: Updated Macaw UI to pre.118
- Updated dependencies [8b3d961]
- Updated dependencies [c50797e]
- @saleor/apps-shared@1.8.0
## 1.8.3
### Patch Changes
- 261957f: Updated dependencies: vite, vitest, eslint, prettier, dotenv, turbo, syncpack, changesets, lint staged
- Updated dependencies [261957f]
- @saleor/apps-shared@1.7.6
## 1.8.2 ## 1.8.2
### Patch Changes ### Patch Changes

View file

@ -36,7 +36,7 @@ Klaviyo app is based on App Template - you can check it [here](https://github.co
3. Install app dependencies with `pnpm i` 3. Install app dependencies with `pnpm i`
4. Start dev server `pnpm dev` 4. Start dev server `pnpm dev`
5. To install app in your Saleor Instance and expose dev server to it, run in the separate terminal `saleor app tunnel` 5. To install app in your Saleor Instance and expose dev server to it, run in the separate terminal `saleor app tunnel`
6. Now you can access the app configuration at [your dashboard]/apps and click on [klavio] 6. Now you can access the app configuration at [your dashboard]/apps and click on [klaviyo]
7. Read how to connect the app with the Klaviyo [here](https://github.com/saleor/saleor-app-template) 7. Read how to connect the app with the Klaviyo [here](https://github.com/saleor/saleor-app-template)
### Local development without CLI ### Local development without CLI

View file

@ -1,4 +1,4 @@
# How to connect your App with Klavio # How to connect your App with Klaviyo
## Installation ## Installation
@ -24,7 +24,7 @@ First and foremost, you need to perform an initial API call to Klaviyo, which wi
Let's navigate to "Customers" and create the first, dummy customer. Let's navigate to "Customers" and create the first, dummy customer.
Then, open Klavio [Metrics page](https://www.klaviyo.com/analytics/metrics). Then, open Klaviyo [Metrics page](https://www.klaviyo.com/analytics/metrics).
Your Metric should be visible on the list: Your Metric should be visible on the list:

View file

@ -1,6 +1,6 @@
{ {
"name": "saleor-app-klaviyo", "name": "saleor-app-klaviyo",
"version": "1.8.2", "version": "1.9.0",
"scripts": { "scripts": {
"build": "pnpm generate && next build", "build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev", "dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
@ -14,12 +14,12 @@
"@material-ui/core": "^4.12.4", "@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3", "@material-ui/icons": "^4.11.3",
"@material-ui/lab": "4.0.0-alpha.61", "@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.41.1", "@saleor/app-sdk": "0.43.1",
"@saleor/apps-shared": "workspace:*", "@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.106", "@saleor/apps-ui": "workspace:*",
"@sentry/nextjs": "7.55.2", "@saleor/macaw-ui": "0.8.0-pre.127",
"@sentry/nextjs": "7.67.0",
"@urql/exchange-auth": "^2.1.4", "@urql/exchange-auth": "^2.1.4",
"clsx": "^1.2.1",
"graphql": "16.7.1", "graphql": "16.7.1",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"next": "13.4.8", "next": "13.4.8",
@ -31,11 +31,11 @@
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"urql": "^4.0.4", "urql": "^4.0.4",
"usehooks-ts": "^2.9.1", "usehooks-ts": "^2.9.1",
"vite": "4.3.9", "vite": "4.4.8",
"vitest": "0.31.3" "vitest": "0.34.1"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "4.0.1", "@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0", "@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/schema-ast": "4.0.0", "@graphql-codegen/schema-ast": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1", "@graphql-codegen/typed-document-node": "5.0.1",
@ -47,7 +47,7 @@
"@types/react-dom": "18.2.5", "@types/react-dom": "18.2.5",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"clean-publish": "^4.0.1", "clean-publish": "^4.0.1",
"eslint": "8.44.0", "eslint": "8.46.0",
"eslint-config-saleor": "workspace:*", "eslint-config-saleor": "workspace:*",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",

View file

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

View file

@ -1,17 +0,0 @@
import { Box } from "@saleor/macaw-ui/next";
import { PropsWithChildren } from "react";
export function AppColumnsLayout({ children }: PropsWithChildren<{}>) {
return (
<Box
display={"grid"}
__gridTemplateColumns={"280px auto 280px"}
gap={4}
__maxWidth={"1180px"}
marginX={"auto"}
marginY={0}
>
{children}
</Box>
);
}

View file

@ -1,11 +1,8 @@
import "@saleor/macaw-ui/next/style";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import React from "react"; import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared";
import { AppProps } from "next/app";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next"; import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NoSSRWrapper } from "@saleor/apps-shared"; import "@saleor/macaw-ui/next/style";
import { ThemeSynchronizer } from "../hooks/theme-synchronizer"; import { AppProps } from "next/app";
/** /**
* Ensure instance is a singleton. * Ensure instance is a singleton.
@ -18,7 +15,9 @@ function SaleorApp({ Component, pageProps }: AppProps) {
<AppBridgeProvider appBridgeInstance={appBridgeInstance}> <AppBridgeProvider appBridgeInstance={appBridgeInstance}>
<ThemeProvider> <ThemeProvider>
<ThemeSynchronizer /> <ThemeSynchronizer />
<Component {...pageProps} /> <Box padding={10}>
<Component {...pageProps} />
</Box>
</ThemeProvider> </ThemeProvider>
</AppBridgeProvider> </AppBridgeProvider>
</NoSSRWrapper> </NoSSRWrapper>

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