Compare commits

..

130 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
Lukasz Ostrowski
8e33f59e0c
Release apps (#835)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-31 12:19:17 +02:00
Lukasz Ostrowski
2cb7e5edee
Improve Search/Algolia mappings (#827)
* parse metadata before sending to algolia

* variants mapping

* extract metadata mapping

* Changeset

* rename field
2023-07-31 12:08:05 +02:00
Lukasz Ostrowski
fa65735571
Release apps (#819)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-28 14:42:27 +02:00
Krzysztof Wolski
07999eadc6
Use select input for AWS region selection (#823)
* Use select input for AWS region selection

* Add changeset
2023-07-28 11:57:51 +02:00
Krzysztof Wolski
43d7e47bd4
EAM: Add AWS credentials validation (#821)
* Add AWS credentials validation

* Add changelog
2023-07-28 11:34:03 +02:00
Krzysztof Wolski
54901f8d08
Eam: Extend order payload (#816)
* Extend the order payload for translated names, attributes and price details

* Add changeset
2023-07-27 16:34:14 +02:00
Adrian Pilarczyk
a725720920
feat: taxes map entity type (#808)
* feat:  add privateMetadata to order in subscription

* feat:  add avatax-entity-type-matcher

* test:  add tests for entity-type-matcher

* refactor: ♻️ use metadata instead of privateMetadata

* refactor: ♻️ replace brs

* chore: 💡 remove todo comment

* build: 👷 add changeset

* refactor: ♻️ graphql queries with metafield instead metadata
2023-07-27 15:23:54 +02:00
Patryk Zawadzki
e9531ce79f
Add Discord to the README (#817) 2023-07-27 12:52:20 +02:00
Krzysztof Wolski
c07ddb33d6
EAM: Fulfillment updated event (#810)
* Add fulfillment update event

* Add changeset

* Improve comments
2023-07-27 10:36:43 +02:00
Lukasz Ostrowski
4c7c1c15d3
Release apps (#793)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-26 13:58:23 +02:00
Adrian Pilarczyk
ce608872bd
feat: disable document recording (#803)
* feat:  add disable document recording

* build: 👷 add changeset

* refactor: ♻️ address feedback

* feat: 💄 increase margin on HelperText

* refactor: ♻️ add default value for isDocumentRecordingEnabled
2023-07-25 15:01:08 +02:00
Adrian Pilarczyk
1b47ad22da
refactor: 🔥 migration code (#806) 2023-07-25 12:30:16 +02:00
Adrian Pilarczyk
5a4da7beed
feat: address validation suggestions (#802)
* feat:  add ping method to avatax-client

* refactor: ♻️ use avatax-auth-validation.service instead of address service

* refactor: ♻️ extract avatax-configuration-address-fragment

* refactor: ♻️ extract avatax-configuration-credentials-fragment

* refactor: ♻️ extract form-helper-text

* refactor: ♻️ extract form-section

* refactor: ♻️ extract avatax-configuration-taxes-fragment

* feat:  move verify to credentials fragment && add disabled form section

* refactor: 🚚 obfuscator

* feat:  add separate credentials and address validation services

* build: 👷 add changeset

* feat:  add address resolution message

* fix: 🐛 changeset

* refactor: ♻️ extract avataxAddressResolutionProcessor and add tests

* refactor:

* refactor: ♻️ remove brs from avatax-instructions

* refactor: ♻️ replace b with Text bodyStrong

* refactor: ♻️ state tuple to object

* refactor: ♻️ destructure some more constructors

* refactor: ♻️ memoize isLoadings & handlers
2023-07-25 11:15:18 +02:00
Lukasz Ostrowski
aa6fec183d
Update Macaw and bindings to pre-106 (#791)
* Bump macaw UI

* Adjust rhf-macaw binding to use new macaw API

* Adjust CRM to use new macaw api

* adjust apps to work with new macaw

* Update combo box

* fix combobox value
2023-07-24 16:45:45 +02:00
Krzysztof Wolski
2fab86b465
Update graphql codegen related packages (#801)
* Update codegen packages

* Add changeset
2023-07-24 11:50:44 +02:00
Lukasz Ostrowski
457865994e
mask api key inputs in builder io form (#792) 2023-07-20 11:02:34 +02:00
Lukasz Ostrowski
5b2976306e
remove CMS v1 app codebase (#789) 2023-07-19 14:01:17 +02:00
Lukasz Ostrowski
3ae7ac6035
Release apps (#749)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-19 11:38:07 +02:00
Lukasz Ostrowski
f6695817a7
cms v1 - notice about deprecation (#787) 2023-07-19 11:26:00 +02:00
Lukasz Ostrowski
5d41af93e1
CMS v2 quality improvements (#782)
* Add required validation for forms

* Add logs

* [skip-ci] handle form errors

* Improve notifications after form created

* add notification when bulk sync finishes

* Add skeletons

* Validation for channel connection form

* cr fixes
2023-07-19 10:22:00 +02:00
Lukasz Ostrowski
44333a6784
Remove monitoring app (#784)
* Remove monitoring app

* remove monitoring cicd
2023-07-19 10:21:39 +02:00
Lukasz Ostrowski
70cb741f88
CMS App v2 (#721)
* initial setup

* GRaphql setup

* [skip ci]

* Manifest and register endpoints

* Add config schemas

* contentful client

* contentful client

* [skip ci] trpc setup

* metadata manager and contentful router

* Configuration config

* contentful config - adding providers

* add provider page

* wip contentful form

* contentful form

* list providrs

* edit contentful form

* [skip ci]

* [skip ci] replace contentful sdk

* replace contentful lib

* Delete contetnful provider feature

* variant created webhook draft

* channel provider connection model

* Channel connections ui

* adding new connection

* connections ui (adding)

* [skip ci] wip edit conn

* removing connection

* rebuild modal

* refactor providers

* implement update product webhook

* add deleting product

* [skip ci] wip

* refactor contentful router

* refactor wip

* refactor config

* webhooks processor

* webhook delegations

* bulk sync section

* bulk sync page

* gql for imports

* [skip ci] bulk import contentful

* temp variant sync list with rate limiters

* wip

* wip

* wip

* new frontend for uploading

* update zod

* print config keys

* wip

* [skip ci] datocms init

* dato add provdier page

* dato form skeleton

* dato  display content type select

* full dato form

* ButtonsBox extraction

* edit dato config form

* update product in dato

* [skip ci]

* extract bulk sync processor

* dato bulk update

* [skip ci] product updated webhook

* product webhook

* crud operations router

* update cruds

* refactor webhook operations

* refactors

* refactors

* helper texts

* [skip ci] deps

* Init

* fix macaw icon

* unify app skd

* unify nextjs

* strapi setup

* fix strapi types

* strapi upload product

* strapi delete product

* strapi product updated webhook

* processor for bulk sync strapi

* shared add provider page

* refactors

* refactors

* wrap providers into folder

* refactors

* refactors

* refactors

* pnpm lock

* add logs

* name configuration mapping name

* form configurable side notes

* remove commentns

* wip providers resolver working

* central config for providers resolving

* tests wip

* remove vscode condig

* cleanup

* provider meta extract

* some tests for contentufl

* contentful client test

* more tests for contentful

* strapi helper texts

* builderio setup

* builderio form

* builderio client draft

* validate connection modal

* Add sending product to builderio

* rename builder field

* add public api key for read access

* update products

* Builder.io - bulk sync

* Fix manifest

* cr fixes

* Make strapi to work with multiple products

* Github actions
2023-07-18 10:19:21 +02:00
Krzysztof Wolski
790a47ee08
Add order refunded webhook (#751)
* Update the app sdk package

* Add order refunded webhook

* Add changeset
2023-07-13 17:50:00 +02:00
Krzysztof Wolski
3c6cd4ccec
Update the app sdk package (#754) 2023-07-13 17:38:04 +02:00
Lukasz Ostrowski
d00ceb6793
Update README.md 2023-07-13 14:45:26 +02:00
Krzysztof Wolski
62104473b4
Update tRPC and React-query (#748)
* Update tRPC and TypeScript

* Fix return types to fix types after the update

* Add changesets

* Use an exact version

* Update react query
2023-07-11 21:32:58 +02:00
Lukasz Ostrowski
e7c2d3a877
Update TS and ESLint (#734) 2023-07-11 17:02:38 +02:00
Lukasz Ostrowski
5473e466f6
Prevent dependabot to trigger Vercel deployments 2023-07-10 11:45:26 +02:00
Lukasz Ostrowski
4162b63a40
Release apps (#690)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-10 11:38:52 +02:00
Krzysztof Wolski
be4e7d2922
Add gift card permission to the manifest and block event (#733)
* Handle missing permissions and old Saleor Version

* Throw an error when fetching app permissions fails
2023-07-10 11:04:21 +02:00
Krzysztof Wolski
d4089ab519
Add metadata and privateMetadata to webhook payloads (#730)
* Add metadata and privateMetadata to webhook payloads

* Add changeset
2023-07-07 13:01:36 +02:00
Krzysztof Wolski
e1980aae90
📧 Prevent users from enabling events without the template (#722)
* Prevent users from enabling events without the template

* Add test for the events update validation
2023-07-06 09:02:08 +02:00
Krzysztof Wolski
78670ce712
📧 Improve log formatting (#724)
* Improve log formatting
2023-07-05 17:17:49 +02:00
Krzysztof Wolski
bda814b7b3
📧 Fix for setting up SMTP transport (#723)
* Fix SMTP transport settings based on encryption

* Stop reporting debug message as error

* Shave some time on fetching configurations

* Add changeset
2023-07-05 11:26:49 +02:00
Lukasz Ostrowski
2d77bca353
Update nextjs to 13.4.8 (#720)
* Update nextjs to 13.4.8

* Update .changeset/neat-queens-draw.md

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>

---------

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
2023-07-04 12:43:57 +02:00
Lukasz Ostrowski
054ec48a66
Allow dependabot to open a daily PR for every package, but limit to 1 (#698) 2023-07-03 15:49:43 +02:00
Adrian Pilarczyk
d2b21cc1ab
feat: tax code create order (#693)
* feat:  add avatax-order-created-tax-code-matcher && extract calculate-taxes matcher

* refactor: 🚚 TaxJarTaxCodeMatcher -> TaxJarCalculateTaxesTaxCodeMatcher

* feat:  add taxjar-order-created-payload-service with tax code

* feat:  add missing tests

* build: 👷 add changeset
2023-07-03 12:56:07 +02:00
Lukasz Ostrowski
47102ba98c
Update menifests to reflect dynamic urls from env (#695) 2023-07-03 11:57:50 +02:00
Lukasz Ostrowski
a1f083c0bc
Add about field in manifest of all apps. Sorted fields (#691)
* Add about field in manifest of all apps. Sorted fields

* Add missing logo

* Update apps/klaviyo/src/pages/api/manifest.ts

Co-authored-by: Adrian Pilarczyk <admin@peelar.dev>

---------

Co-authored-by: Adrian Pilarczyk <admin@peelar.dev>
2023-06-30 14:01:46 +02:00
Lukasz Ostrowski
6299e064b6
Update app-sdk (#672)
* Update app-sdk to dev release and temporary display saleor versions in search app

* Update app-sdk

* add changeset

* Removed temp code
2023-06-29 14:35:57 +02:00
Lukasz Ostrowski
7c98e384fd
E2E tests (#668)
* playwright install

* Configrations

* assertions

* basic tests for apps installations

* tests for product feed

* Adjust PF assertions to use test-id

* Taxes smoke test

* moved files around

* Add smoke for Klaviyo

* More taxes tests

* remove workflow

* add example

* extract separate test for pf

* Improve PF test

* cr fixes
2023-06-28 17:08:00 +02:00
Lukasz Ostrowski
d34c2a1b84
Release apps (#685)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-28 16:45:40 +02:00
Lukasz Ostrowski
cbd763bfdd
Disabled Klaviyo ssr (#684) 2023-06-28 16:04:59 +02:00
Lukasz Ostrowski
9b65569ec3
Release apps (#556)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-28 15:17:54 +02:00
Adrian Pilarczyk
d42c79f366
fix: tax code matcher QA (#680)
* feat:  bring back validateAddress in TaxJar

* feat: 💄 decrease marginLeft of AppToggle label

* refactor: 🚚 cancelButton -> leftButton

* feat: 🧱 add data-testid to all buttons and forms

* refactor: ♻️ refactor app-section to accept Box props

* feat: 🧱 add rest of data-testid

* feat:  verify connections before displaying matcher pages

* feat:  always display matcher-section

* refactor: ♻️ improve fetching tax codes by adding retry and redirect on error

* refactor: 🚚 active-connection -> get-active-connection-service && improve logs

* chore: 🔊 remove objects from logs

* docs: 📝 add TESTING.md with edge-cases
2023-06-28 15:03:24 +02:00
Krzysztof Wolski
6250095a4e
📧 Introduce new event - Gift Card Sent (#661)
* Introduce new event - Gift Card Sent

* Add feature flag service and use it with gift card event

* Add saleor version check on install
2023-06-28 11:39:26 +02:00
Adrian Pilarczyk
1405deaf66
chore: 🗃️ modify migration scripts to overwrite (#681) 2023-06-28 09:58:31 +02:00
Lukasz Ostrowski
37e50db29c
update macaw in klaviyo (#670)
* update macaw in klaviyo

* cr fixes

* removed e2e artifacts
2023-06-28 09:45:35 +02:00
Lukasz Ostrowski
3bd7e3f05d
Monitoring app UI (#677)
* Replace macaw to next

* UI WIP

* ui wip

* fix graphql calls

* Fix ui

* Changsets

* Apply CR review

* fix spacing
2023-06-27 19:20:58 +02:00
Lukasz Ostrowski
f6ff907cfd Improve test-id for product feed 2023-06-27 12:58:59 +02:00
Lukasz Ostrowski
8b245c6bcf
Improve errors handling in Invoices app (#664)
* Remove migration code, add Sentry breadcrumbs

* Add Sentry breadcrumbs to invoice created webhook

* Add changeset

* remove insecure logs
2023-06-27 12:13:04 +02:00
Lukasz Ostrowski
e4497b9ba4
Add test-ids to product feed (#669)
* add test-ids to product feed

* add dynamic google form

* Update .changeset/poor-hounds-move.md

Co-authored-by: Adrian Pilarczyk <admin@peelar.dev>

---------

Co-authored-by: Adrian Pilarczyk <admin@peelar.dev>
2023-06-27 12:10:19 +02:00
Lukasz Ostrowski
57fea9166e
Redesign Slack app to use new Macaw / next (#671)
* Replace material with new Macaw ui

* Rewrtirren Slack to the new macaw

* CR fixes and imprve layour
2023-06-27 12:07:24 +02:00
1024 changed files with 155470 additions and 57684 deletions

View file

@ -1,5 +0,0 @@
---
"saleor-app-taxes": minor
---
Redesigned the app's UI with the new version of macaw-ui. Introduced breaking changes in the structure of providers configuration and channels configuration. Added migrations that convert the obsolete configurations to the new format. Added address validation for tax providers.

View file

@ -1,15 +0,0 @@
---
"saleor-app-emails-and-messages": minor
"saleor-app-data-importer": minor
"saleor-app-products-feed": minor
"saleor-app-monitoring": minor
"saleor-app-invoices": minor
"saleor-app-klaviyo": minor
"saleor-app-search": minor
"saleor-app-slack": minor
"saleor-app-taxes": minor
"saleor-app-cms": minor
"saleor-app-crm": minor
---
Included dedicated logo and attached it to App's manifest. From Saleor 3.15 the logo will be visible in the Dashboard during and after installation.

View file

@ -1,5 +0,0 @@
---
"saleor-app-taxes": patch
---
The update provider configuration views no longer return "invalid credentials" and "invalid address" errors in inappropriate cases. The latter required temporarily disabling the TaxJar address validation, as it currently doesn't work.

View file

@ -1,5 +0,0 @@
---
"saleor-app-taxes": minor
---
Added fetching tax codes from tax providers and storing the matched combinations of the provider tax codes/Saleor tax classes. The mapped tax codes are then used in the tax calculation process.

View file

@ -1,5 +0,0 @@
---
"@saleor/react-hook-form-macaw": minor
---
Added a binding for the macaw-ui's Toggle component.

View file

@ -1,5 +0,0 @@
---
"saleor-app-products-feed": minor
---
Improved helper text in S3 form - region field. Now it should be more explicit that only region code (like "eu-west-1") should be provided.

View file

@ -1,5 +0,0 @@
---
"@saleor/apps-ui": patch
---
Fixed a missing text-decoration on a breadcrumb link.

View file

@ -1,6 +0,0 @@
---
"@saleor/apps-cli": patch
---
Introduced Apps CLI.
The app is intended to help with common tasks in app development, like installing apps from manifest or debugging webhooks.

View file

@ -1,8 +0,0 @@
---
"saleor-app-emails-and-messages": patch
"saleor-app-invoices": patch
"saleor-app-crm": patch
---
Fixed "Not enough permissions" error during configuration management.

View file

@ -1,17 +0,0 @@
---
"saleor-app-emails-and-messages": patch
"saleor-app-data-importer": patch
"saleor-app-products-feed": patch
"saleor-app-monitoring": patch
"@saleor/apps-shared": patch
"saleor-app-invoices": patch
"saleor-app-klaviyo": patch
"saleor-app-search": patch
"@saleor/apps-ui": patch
"saleor-app-slack": patch
"saleor-app-taxes": patch
"saleor-app-cms": patch
"saleor-app-crm": patch
---
Updated @saleor/app-sdk to 0.40.1

View file

@ -1,17 +0,0 @@
---
"saleor-app-emails-and-messages": patch
"saleor-app-data-importer": patch
"saleor-app-products-feed": patch
"saleor-app-monitoring": patch
"@saleor/apps-shared": patch
"saleor-app-invoices": patch
"saleor-app-klaviyo": patch
"saleor-app-search": patch
"saleor-app-slack": patch
"saleor-app-taxes": patch
"saleor-app-cms": patch
"saleor-app-crm": patch
---
Updated configuration and dependencies of GraphQL client - urql.
All applications use now unified config for creating the client. Also unused related packages has been removed.

View file

@ -1,5 +0,0 @@
---
"saleor-app-emails-and-messages": patch
---
Improved descriptions in the events section and API key inputs.

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

@ -1,14 +0,0 @@
---
"saleor-app-emails-and-messages": patch
"saleor-app-data-importer": patch
"saleor-app-products-feed": patch
"saleor-app-invoices": patch
"saleor-app-klaviyo": patch
"saleor-app-search": patch
"saleor-app-slack": patch
"saleor-app-taxes": patch
"saleor-app-cms": patch
"saleor-app-crm": patch
---
Updated Sentry package and config. Set Sentry release to match package.json version. Now Sentry will use semver version instead a commit

View file

@ -1,19 +0,0 @@
---
"@saleor/react-hook-form-macaw": patch
"eslint-config-saleor": patch
"saleor-app-emails-and-messages": patch
"saleor-app-data-importer": patch
"saleor-app-products-feed": patch
"saleor-app-monitoring": patch
"@saleor/apps-shared": patch
"saleor-app-invoices": patch
"saleor-app-klaviyo": patch
"saleor-app-search": patch
"@saleor/apps-ui": patch
"saleor-app-slack": patch
"saleor-app-taxes": patch
"saleor-app-cms": patch
"saleor-app-crm": patch
---
Updated dependencies

View file

@ -1,5 +0,0 @@
---
"@saleor/apps-shared": patch
---
Added `createGraphQLClient` function to shared package. Can be used to create urql client instance with optional authorization.

View file

@ -1,5 +0,0 @@
---
"saleor-app-search": patch
---
Regression - Fixed Algolia "index prefix" field to be optional, just like it was before the refcator.

View file

@ -1,7 +0,0 @@
---
"saleor-app-emails-and-messages": minor
---
Webhooks are no longer created during the app registration. Instead, the app will subscribe events based on it's configuration, after change has been detected.
This change does not have negative impact on existing app installations - webhooks will be removed during next change of the provider configuration.

View file

@ -1,5 +0,0 @@
---
"saleor-app-taxes": patch
---
Fixed the issue with updating a provider. When updating one of the credentials, the user no longer sees "invalid credentials" error.

View file

@ -1,5 +0,0 @@
---
"saleor-app-emails-and-messages": patch
---
Events section UI has been updated. All events are displayed now as single table.

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,100 +2,18 @@ version: 2
updates: updates:
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
open-pull-requests-limit: 0
schedule: schedule:
interval: "weekly" interval: "daily"
commit-message:
prefix: "[skip ci]"
groups:
trpc:
patterns:
- "@trpc/*"
codegen:
patterns:
- "@graphql-codegen/*"
rtl:
patterns:
- "@testing-library/*"
# Packages
- package-ecosystem: "npm"
directory: "/packages/eslint-config-saleor"
open-pull-requests-limit: 0
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/shared"
open-pull-requests-limit: 0
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/ui"
open-pull-requests-limit: 0
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/packages/react-hook-form-macaw"
open-pull-requests-limit: 0
schedule:
interval: "weekly"
# Apps
- package-ecosystem: "npm"
directory: "/apps/apps-cli"
open-pull-requests-limit: 0
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/apps/cms"
open-pull-requests-limit: 0
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: apps/crm
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/data-importer
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/emails-and-messages
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/invoices
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/klaviyo
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/monitoring
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "pip"
directory: apps/monitoring/backend
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "docker"
directory: apps/monitoring/backend
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/products-feed
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/search
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
directory: apps/slack
open-pull-requests-limit: 0
schedule:
interval: weekly
- package-ecosystem: "npm"
open-pull-requests-limit: 0
directory: apps/taxes
schedule:
interval: weekly

4
.github/labeler.yml vendored
View file

@ -1,11 +1,11 @@
"App: Data Importer": "apps/data-importer/**/*" "App: Data Importer": "apps/data-importer/**/*"
"App: Invoices": "apps/invoices/**/*" "App: Invoices": "apps/invoices/**/*"
"App: Klaviyo": "apps/klaviyo/**/*" "App: Klaviyo": "apps/klaviyo/**/*"
"App: Monitoring": "apps/monitoring/**/*"
"App: Product Feed": "apps/products-feed/**/*" "App: Product Feed": "apps/products-feed/**/*"
"App: Search": "apps/search/**/*" "App: Search": "apps/search/**/*"
"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/**/*" "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"

View file

@ -1,81 +0,0 @@
name: Publish image
on:
push:
branches:
- main
paths:
- apps/monitoring/backend/**
jobs:
publish:
runs-on: ubuntu-22.04
env:
AWS_REGION: eu-west-1
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
install: true
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_APPS_STAGING_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_APPS_STAGING_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- id: ecr-login
name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v1
with:
registries: ${{ secrets.AWS_ECR_ACCOUNT }}
- name: Evaluate image tags
run: |
IMAGE_REPOSITORY=${{ steps.ecr-login.outputs.registry }}/${{ secrets.ECR_REPOSITORY }}
BRANCH_IMAGE_TAG=${{ github.ref_name }}
UNIQUE_IMAGE_TAG=${BRANCH_IMAGE_TAG}-$(git rev-parse --short HEAD)
IMAGE_TAGS=${IMAGE_REPOSITORY}:${BRANCH_IMAGE_TAG},${IMAGE_REPOSITORY}:${UNIQUE_IMAGE_TAG}
echo "UNIQUE_IMAGE_TAG=${UNIQUE_IMAGE_TAG}" >> $GITHUB_ENV
echo "IMAGE_TAGS=${IMAGE_TAGS}" >> $GITHUB_ENV
- name: Build and push
timeout-minutes: 20
uses: docker/build-push-action@v4
with:
context: ./apps/monitoring/backend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.IMAGE_TAGS }}
target: prod
cache-from: type=gha,scope=buildkit-master
cache-to: type=gha,scope=buildkit-master
# - name: Configure GitHub credentials
# run: |
# GITHUB_TOKEN=$( \
# curl --request GET --url ${{ secrets.VAULT_URL}} --header "Authorization: JWT ${{ secrets.VAULT_JWT }}" | jq -r .token \
# )
# echo "GITHUB_TOKEN=${GITHUB_TOKEN}" >> $GITHUB_ENV
# - name: Trigger Helm deployment
# run: |
# gh api /repos/saleor/saleor-cloud-deployments/dispatches \
# --input - <<< '{
# "event_type": "deploy-app-monitoring-staging",
# "client_payload": {
# "image_tag": "${{ env.UNIQUE_IMAGE_TAG }}"
# }
# }'

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

@ -1,33 +0,0 @@
name: "App: Monitoring backend tests"
on:
pull_request:
paths:
- "apps/monitoring/backend/**"
jobs:
unit_test:
name: Unit tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/monitoring/backend
steps:
- uses: actions/checkout@v3
- name: Install poetry
run: pipx install poetry
- name: Setup python
uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: poetry
- name: Install dependencies
run: poetry install
- name: Run unit tests
run: poetry run pytest
- name: Run black
run: poetry run black .
- name: Run ruff
run: poetry run ruff .
- name: Run mypy
run: poetry run mypy .

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

@ -41,3 +41,6 @@ apps/**/generated
.eslintcache .eslintcache
.sentryclirc .sentryclirc
.vscode/
.cspellcache

View file

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

View file

@ -16,6 +16,8 @@
<a href="https://saleor.io/blog/">📰 Blog</a> <a href="https://saleor.io/blog/">📰 Blog</a>
<span></span> <span></span>
<a href="https://twitter.com/getsaleor">🐦 Twitter</a> <a href="https://twitter.com/getsaleor">🐦 Twitter</a>
<span></span>
<a href="https://discord.gg/H52JTZAtSH">💬 Discord</a>
</div> </div>
<div align="center"> <div align="center">
@ -26,6 +28,13 @@
<a href="https://github.com/saleor/apps/discussions/categories/integrations-features">✍️ Propose an app</a> <a href="https://github.com/saleor/apps/discussions/categories/integrations-features">✍️ Propose an app</a>
</div> </div>
<br/>
<div align="center">
[![Discord Badge](https://dcbadge.vercel.app/api/server/H52JTZAtSH)](https://discord.gg/H52JTZAtSH)
</div>
## Overview ## Overview
This repository serves as a starting point in the exploration of Saleor apps. This repository serves as a starting point in the exploration of Saleor apps.
@ -39,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
@ -50,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,3 +0,0 @@
INSTANCE_URL=https://demo.saleor.io/graphql/
USER_EMAIL=admin@example.com
USER_PASSWORD=password123

View file

@ -1,148 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
package-lock.json
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# other
build/
.vscode/
binaries/
.saleor
token.txt
vendor/
test.js
dummy/
..bfg-report
.idea/
.DS_Store
package/
.type-coverage/
coverage-ts/

View file

@ -1,37 +0,0 @@
### Install dependencies
This project uses [pnpm](https://pnpm.io) for managing dependencies
```
pnpm install
```
### Run Watch Mode
```
pnpm watch
```
### Run CLI
```
node build/cli.js ...
```
### Available commands
List of available commands:
```
node dist/app-cli.js -h
```
Description of available arguments:
```
node dist/app-cli.js [command name] -h
```
### Configuration
If options are not passed as arguments, cli will try to read environment variables. Example configuration is available in `.env.example` file.

View file

@ -1,18 +0,0 @@
/* eslint-disable import/no-default-export */
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "https://demo.saleor.io/graphql/",
documents: ["src/saleor-api/operations/*.ts"],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
"./src/saleor-api/generated/": {
preset: "client",
presetConfig: {
fragmentMasking: false,
},
},
},
};
export default config;

View file

@ -1,67 +0,0 @@
{
"name": "@saleor/apps-cli",
"description": "",
"version": "0.0.0",
"author": "Saleor",
"scripts": {
"build": "pnpm bundle",
"generate": "graphql-codegen",
"lint": "prettier --write . && eslint src/**/*.ts --cache --fix",
"test": "pnpm vitest",
"typecov": "type-coverage --cache",
"typecov-report": "typescript-coverage-report",
"watch": "concurrently \"npm:watch-*\"",
"watch-esbuild": "esbuild --watch src/cli.ts --bundle --minify --outfile=dist/apps-cli.js --platform=node --format=esm --target=node18 --banner:js=\"import { createRequire } from 'module';const require = createRequire(import.meta.url);import { dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url));\" --out-extension:.js=.js",
"watch-generate": "graphql-codegen -w",
"watch-ts": "tsc --noEmit --watch --preserveWatchOutput"
},
"bin": {
"saleor": "./dist/apps-cli.js"
},
"dependencies": {
"@graphql-typed-document-node/core": "3.2.0",
"@inquirer/prompts": "^2.1.1",
"@oclif/core": "^1.26.2",
"@saleor/app-sdk": "0.40.1",
"chalk": "^5.2.0",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"enquirer": "^2.3.6",
"fs-extra": "^11.1.1",
"graphql": "^16.6.0",
"graphql-request": "^6.1.0",
"open": "^9.1.0",
"ora": "^6.3.1",
"semver": "^7.5.1",
"slugify": "^1.6.6",
"yargs": "^17.7.2"
},
"devDependencies": {
"@graphql-codegen/cli": "^4.0.1",
"@graphql-codegen/client-preset": "^4.0.1",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.3.1",
"@types/semver": "^7.5.0",
"@types/yargs": "^17.0.24",
"concurrently": "^8.2.0",
"esbuild": "^0.18.2",
"eslint": "^8.42.0",
"eslint-config-saleor": "workspace:*",
"pkg": "^5.8.1",
"prettier": "2.8.8",
"ts-node": "^10.9.1",
"tsm": "^2.3.0",
"type-coverage": "^2.26.0",
"typescript": "^5.1.3",
"typescript-coverage-report": "^0.7.0",
"vitest": "^0.32.0"
},
"engines": {
"node": "^18 || ^20"
},
"files": [
"dist/apps-cli.js"
],
"license": "BSD 3-Clause",
"type": "module"
}

View file

@ -1,158 +0,0 @@
#!/usr/bin/env node
import chalk from "chalk";
import { createRequire } from "module";
import semver from "semver";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { installAppCommand } from "./commands/install-app-command";
import { uninstallAppCommand } from "./commands/uninstall-app-command";
import "dotenv/config";
import { webhooksCommand } from "./commands/webhooks-command";
const require = createRequire(import.meta.url);
const pkg = require("../package.json");
if (!semver.satisfies(process.versions.node, ">= 18")) {
console.error(`${chalk.red("ERROR")}: CLI requires Node.js 18.x or later`);
process.exit(1);
}
const parser = yargs(hideBin(process.argv))
.scriptName("apps-cli")
.version(pkg.version)
.alias("V", "version")
.usage("Usage: $0 <command> [options]")
.config({
instanceUrl: process.env.INSTANCE_URL,
userEmail: process.env.USER_EMAIL,
userPassword: process.env.USER_PASSWORD,
})
.command(
"installApp",
"Install an app on a Saleor instance based on provided manifest.",
(yargs) => {
return yargs
.option("instanceUrl", {
type: "string",
desc: "URL to the Saleor GraphQL API. Example: https://example.com/graphql/",
demandOption: true,
})
.option("userEmail", {
type: "string",
desc: "Dashboard user email",
demandOption: true,
})
.option("userPassword", {
type: "string",
desc: "Dashboard user password",
demandOption: true,
})
.option("manifestUrl", {
type: "string",
desc: "URL to the app manifest. Example: https://example.com/api/manifest",
demandOption: true,
});
},
(argv) => {
installAppCommand({
instanceUrl: argv.instanceUrl,
userEmail: argv.userEmail,
userPassword: argv.userPassword,
manifestUrl: argv.manifestUrl,
});
}
)
.command(
"uninstallApp",
"If no filter is passed, CLI will display a list of installed apps and ask which one to remove. Otherwise all apps matching the filter will be removed.",
(yargs) => {
return yargs
.option("instanceUrl", {
type: "string",
desc: "URL to the Saleor GraphQL API",
demandOption: true,
})
.option("userEmail", {
type: "string",
desc: "Dashboard user email",
demandOption: true,
})
.option("userPassword", {
type: "string",
desc: "Dashboard user password",
demandOption: true,
})
.option("manifestUrl", {
type: "string",
desc: "Url to the app manifest which you want to remove",
})
.option("appName", {
type: "string",
desc: "Name of the app to remove",
})
.option("appId", {
type: "string",
desc: "If of the app to remove",
})
.option("all", {
type: "boolean",
default: false,
desc: "Will remove all apps",
})
.option("force", {
type: "boolean",
default: false,
desc: "No confirmation",
});
},
(argv) => {
uninstallAppCommand({
instanceUrl: argv.instanceUrl,
userEmail: argv.userEmail,
userPassword: argv.userPassword,
manifestUrl: argv.manifestUrl,
appId: argv.appId,
all: argv.all,
force: argv.force,
});
}
)
.command(
"webhooks",
"Print webhook details of installed app.",
(yargs) => {
return yargs
.option("instanceUrl", {
type: "string",
desc: "URL to the Saleor GraphQL API. Example: https://example.com/graphql/",
demandOption: true,
})
.option("userEmail", {
type: "string",
desc: "Dashboard user email",
demandOption: true,
})
.option("userPassword", {
type: "string",
desc: "Dashboard user password",
demandOption: true,
});
},
(argv) => {
webhooksCommand({
instanceUrl: argv.instanceUrl,
userEmail: argv.userEmail,
userPassword: argv.userPassword,
});
}
)
.demandCommand(1, "You need at least one command before moving on")
.alias("h", "help")
.wrap(null);
try {
await parser.parse();
} catch (error) {
console.log("parser error");
}

View file

@ -1,35 +0,0 @@
import { installAndWaitForResult } from "../lib/install-and-wait-for-result";
import { getAccessTokenMutation } from "../saleor-api/operations/get-access-token-mutation";
import ora from "ora";
interface InstallAppCommandArgs {
instanceUrl: string;
userEmail: string;
userPassword: string;
manifestUrl: string;
}
export const installAppCommand = async ({
instanceUrl,
manifestUrl,
userEmail,
userPassword,
}: InstallAppCommandArgs) => {
const loginSpinner = ora("Logging into Saleor instance").start();
const token = await getAccessTokenMutation({
email: userEmail,
password: userPassword,
saleorApiUrl: instanceUrl,
});
loginSpinner.succeed();
const installedAppData = await installAndWaitForResult({
saleorApiUrl: instanceUrl,
token,
appManifestUrl: manifestUrl,
});
console.log(`App ${installedAppData.name} (${installedAppData.id}) installed!`);
};

View file

@ -1,110 +0,0 @@
import { checkbox, confirm } from "@inquirer/prompts";
import { getAccessTokenMutation } from "../saleor-api/operations/get-access-token-mutation";
import { getAppsListQuery } from "../saleor-api/operations/get-apps-list-query";
import { uninstallAppMutation } from "../saleor-api/operations/uninstall-app-mutation";
import { filterApps } from "../lib/filter-apps";
import ora from "ora";
interface UninstallAppCommandArgs {
instanceUrl: string;
userEmail: string;
userPassword: string;
manifestUrl?: string;
appName?: string;
appId?: string;
all?: boolean;
force?: boolean;
}
export const uninstallAppCommand = async ({
instanceUrl,
manifestUrl,
userEmail,
userPassword,
all,
force,
appId,
appName,
}: UninstallAppCommandArgs) => {
const loginSpinner = ora("Logging into Saleor instance").start();
const token = await getAccessTokenMutation({
email: userEmail,
password: userPassword,
saleorApiUrl: instanceUrl,
});
loginSpinner.succeed();
const appIdsToRemove: string[] = [];
if (appId) {
appIdsToRemove.push(appId);
} else {
const appListSpinner = ora("Fetching installed apps").start();
const installedApps = await getAppsListQuery({
saleorApiUrl: instanceUrl,
token,
});
appListSpinner.succeed();
if (!installedApps.length) {
console.log("No apps installed");
return;
}
// Display CLI interface with multiselect if none of the filters were provided
if (appId || appName || manifestUrl) {
const filteredApps = filterApps({
apps: installedApps,
filter: {
id: appId,
name: appName,
manifestUrl: manifestUrl,
},
});
appIdsToRemove.push(...filteredApps.map((app) => app.id));
} else if (all) {
appIdsToRemove.push(...installedApps.map((app) => app.id));
} else {
const selectedIds = await checkbox({
message: "Select apps to uninstall",
choices: installedApps.map((app) => ({
name: app.name ? `${app.name} (${app.id}) ${app.type}` : app.id,
value: app.id,
})),
});
appIdsToRemove.push(...selectedIds);
}
}
const confirmed = force
? true
: await confirm({
message: `${appIdsToRemove.length} apps will be removed. Continue?`,
default: false,
});
if (!confirmed) {
console.log("Operation aborted - no confirmation");
return;
}
const uninstallSpinner = ora("Uninstalling apps").start();
try {
await Promise.all(
appIdsToRemove.map((appId) =>
uninstallAppMutation({ saleorApiUrl: instanceUrl, token, id: appId })
)
);
} catch (e) {
uninstallSpinner.fail();
console.error(e);
return;
}
uninstallSpinner.succeed();
};

View file

@ -1,120 +0,0 @@
import ora from "ora";
import { getAccessTokenMutation } from "../saleor-api/operations/get-access-token-mutation";
import { getAppsListQuery } from "../saleor-api/operations/get-apps-list-query";
import { select } from "@inquirer/prompts";
import { getAppWebhooksQuery } from "../saleor-api/operations/get-app-webhooks-query";
import { removeWebhookMutation } from "../saleor-api/operations/remove-webhook-mutation";
interface DumpMetadataCommandArgs {
instanceUrl: string;
userEmail: string;
userPassword: string;
}
export const webhooksCommand = async ({
instanceUrl,
userEmail,
userPassword,
}: DumpMetadataCommandArgs) => {
const loginSpinner = ora("Logging into Saleor instance").start();
const token = await getAccessTokenMutation({
email: userEmail,
password: userPassword,
saleorApiUrl: instanceUrl,
});
loginSpinner.succeed();
const appListSpinner = ora("Fetching installed apps").start();
const installedApps = await getAppsListQuery({
saleorApiUrl: instanceUrl,
token,
});
appListSpinner.succeed();
if (!installedApps.length) {
console.log("No apps installed");
return;
}
const appId = await select({
message: "Select app",
choices: installedApps.map((app) => ({
name: app.name ? `${app.name} (${app.id})` : app.id,
value: app.id,
})),
});
const webhooksData = await getAppWebhooksQuery({
appId,
saleorApiUrl: instanceUrl,
token,
});
if (!webhooksData.length) {
console.log("Application has no webhooks configured");
return;
}
const webhook = await select({
message: "Select webhook to investigate",
choices: webhooksData.map((webhook) => ({
name: `${webhook.name} (${[...webhook.syncEvents, ...webhook.asyncEvents]
.map((e) => e.name)
.join(", ")})`,
value: webhook,
description: `
Target url: ${webhook.targetUrl}
Active: ${webhook.isActive}
Captured event deliveries count: ${webhook.eventDeliveries?.edges.length}
`,
})),
});
const operation = await select({
message: "Operation",
choices: [
{
name: "List event deliveries",
value: "list",
},
{
name: "Remove webhook",
value: "remove",
},
],
});
if (operation === "list") {
console.log("Number of entries: ", webhook.eventDeliveries?.edges.length);
for (const deliveryEdge of webhook.eventDeliveries?.edges ?? []) {
const delivery = deliveryEdge.node;
console.log(`
Event type: ${delivery.eventType}
Created at: ${delivery.createdAt}
Status: ${delivery.status}`);
const attempts = delivery.attempts?.edges ?? [];
const lastAttempt = attempts[attempts.length - 1]?.node;
if (lastAttempt) {
console.log(`
Date of the last attempt: ${lastAttempt.createdAt}
Status: ${lastAttempt.status}`);
}
}
} else if (operation === "remove") {
const removeSpinner = ora("Removing webhook...").start();
await removeWebhookMutation({
saleorApiUrl: instanceUrl,
token,
webhookId: webhook.id,
});
removeSpinner.succeed();
}
};

View file

@ -1,21 +0,0 @@
import { AppManifest } from "@saleor/app-sdk/types";
export const fetchAppManifest = async (manifestUrl: string) => {
const manifestDataResponse = await fetch(manifestUrl);
let manifestData: AppManifest;
if (!manifestDataResponse.ok) {
console.log("Error fetching manifest");
throw new Error("Error fetching manifest");
}
try {
manifestData = (await manifestDataResponse.json()) as AppManifest;
} catch (e) {
console.log("Error parsing manifest");
throw new Error("Error parsing manifest");
}
return manifestData;
};

View file

@ -1,65 +0,0 @@
import { describe, expect, it } from "vitest";
import { AppDetailsFragment } from "../saleor-api/generated/graphql";
import { filterApps } from "./filter-apps";
const mockedApp1: AppDetailsFragment = {
id: "1",
name: "app1",
manifestUrl: "https://app1.com",
};
const mockedApp1Duplicate: AppDetailsFragment = {
id: "2",
name: "app1",
manifestUrl: "https://app1.com",
};
const mockedApp2: AppDetailsFragment = {
id: "3",
name: "app2",
manifestUrl: "https://app2.com",
};
const mockedAppList = [mockedApp1, mockedApp1Duplicate, mockedApp2];
describe("filterApps", function () {
it("Return the same apps, when no filters applied", async () => {
expect(
filterApps({
apps: mockedAppList,
filter: {},
})
).toStrictEqual(mockedAppList);
});
it("Return all apps with the same name, when filter name is applied", async () => {
expect(
filterApps({
apps: mockedAppList,
filter: {
name: mockedApp1.name!,
},
})
).toStrictEqual([mockedApp1, mockedApp1Duplicate]);
});
it("Return all apps with the same manifest, when filter manifest is applied", async () => {
expect(
filterApps({
apps: mockedAppList,
filter: {
manifestUrl: mockedApp1.manifestUrl!,
},
})
).toStrictEqual([mockedApp1, mockedApp1Duplicate]);
});
it("Return app with given id, when filter id is applied", async () => {
expect(
filterApps({
apps: mockedAppList,
filter: {
id: mockedApp1.id,
},
})
).toStrictEqual([mockedApp1]);
});
});

View file

@ -1,28 +0,0 @@
import { AppDetailsFragment } from "../saleor-api/generated/graphql";
interface FilterAppsArgs {
apps: AppDetailsFragment[];
filter: {
id?: string;
name?: string;
manifestUrl?: string;
};
}
export const filterApps = ({ apps, filter: { id, manifestUrl, name } }: FilterAppsArgs) => {
return apps.filter((app) => {
if (id && app.id !== id) {
return false;
}
if (name && app.name !== name) {
return false;
}
if (manifestUrl && app.manifestUrl !== manifestUrl) {
return false;
}
return true;
});
};

View file

@ -1,94 +0,0 @@
import ora from "ora";
import { fetchAppManifest } from "./fetch-app-manifest";
import { filterApps } from "./filter-apps";
import { getAppInstallationsListQuery } from "../saleor-api/operations/get-app-installations-list-query";
import { getAppsListQuery } from "../saleor-api/operations/get-apps-list-query";
import { installAppMutation } from "../saleor-api/operations/install-app-mutation";
interface InstallAndWaitForResultArgs {
saleorApiUrl: string;
token: string;
appManifestUrl: string;
}
function delay(timeMs: number) {
return new Promise((resolve) => setTimeout(resolve, timeMs));
}
/*
* Attempt to install app from the manifest, wait for the operation to complete
* and return app installation result.
* If will throw error if any of the steps fails.
*/
export const installAndWaitForResult = async ({
saleorApiUrl,
token,
appManifestUrl,
}: InstallAndWaitForResultArgs) => {
const manifestSpinner = ora("Fetching app manifest").start();
const manifestData = await fetchAppManifest(appManifestUrl);
manifestSpinner.succeed();
const installSpinner = ora("Installing the app").start();
const appInstallationJob = await installAppMutation({
manifestUrl: appManifestUrl,
saleorApiUrl: saleorApiUrl,
token,
appName: manifestData.name,
});
installSpinner.text = `Installing the app (job id: ${appInstallationJob.id})`;
// Lets give the API a bit of time to process installation
await delay(1000);
// App installation is on progress, now we have to monitor if it resolved. Wait max 20s for the result
for (let i = 0; i < 10; i++) {
const currentAppInstallations = await getAppInstallationsListQuery({
saleorApiUrl: saleorApiUrl,
token,
});
const appInstallation = currentAppInstallations.find((x) => x.id === appInstallationJob.id);
if (!appInstallation) {
// Job has been processed! If not on the list, it means it was successful
break;
}
if (appInstallation.status === "FAILED") {
installSpinner.fail("Installation failed");
throw new Error("App installation failed: " + appInstallation.message);
}
// Wait a bit and check again
await delay(2000);
}
installSpinner.text = "Confirming the app installed";
// App should be installed by now, fetch its details
const currentAppInstallations = await getAppsListQuery({
saleorApiUrl,
token,
});
const installedApp = filterApps({
apps: currentAppInstallations,
filter: {
manifestUrl: appManifestUrl,
},
});
if (!installedApp.length) {
// Investigate if this can happen - app not in the list of installed apps nor in the list of installations
throw new Error("App not found on the list of installed apps");
}
installSpinner.succeed("App installed!");
return installedApp[0];
};

View file

@ -1,42 +0,0 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const getAccessTokenMutationDocument = graphql(/* GraphQL */ `
mutation GetAccessToken($email: String!, $password: String!) {
tokenCreate(email: $email, password: $password) {
token
refreshToken
errors {
field
message
}
}
}
`);
export const getAccessTokenMutation = async ({
saleorApiUrl,
email,
password,
}: {
saleorApiUrl: string;
password: string;
email: string;
}) => {
const { tokenCreate } = await request(saleorApiUrl, getAccessTokenMutationDocument, {
email,
password,
});
if (tokenCreate?.errors.length) {
console.log("mutation failed", tokenCreate?.errors);
throw new Error(`Get access token mutation failed - API returned errors`);
}
const token = tokenCreate?.token;
if (!token) {
throw new Error(`Get access token mutation failed - no token in the response`);
}
return token;
};

View file

@ -1,30 +0,0 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const getAppInstallationsQueryDocument = graphql(/* GraphQL */ `
query GetAppInstallations {
appsInstallations {
id
status
message
}
}
`);
export const getAppInstallationsListQuery = async ({
saleorApiUrl,
token,
}: {
saleorApiUrl: string;
token: string;
}) => {
const { appsInstallations } = await request(
saleorApiUrl,
getAppInstallationsQueryDocument,
{},
{ "Authorization-Bearer": token }
);
return appsInstallations;
};

View file

@ -1,31 +0,0 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const getAppMetadataQueryDocument = graphql(/* GraphQL */ `
query GetAppMetadata {
app(id: "QXBwOjE=") {
metadata {
key
value
}
}
}
`);
export const getAppMetadataQuery = async ({
saleorApiUrl,
token,
}: {
saleorApiUrl: string;
token: string;
}) => {
const { app } = await request(
saleorApiUrl,
getAppMetadataQueryDocument,
{},
{ "Authorization-Bearer": token }
);
return app?.metadata ?? [];
};

View file

@ -1,67 +0,0 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const getAppWebhooksQueryDocument = graphql(/* GraphQL */ `
query GetAppWebhooks($id: ID!) {
app(id: $id) {
webhooks {
id
name
isActive
syncEvents {
name
eventType
}
asyncEvents {
name
eventType
}
targetUrl
eventDeliveries(first: 10) {
edges {
node {
id
createdAt
status
eventType
attempts(first: 10) {
edges {
node {
id
createdAt
taskId
duration
response
status
}
}
}
}
}
}
}
}
}
`);
export const getAppWebhooksQuery = async ({
saleorApiUrl,
token,
appId,
}: {
saleorApiUrl: string;
token: string;
appId: string;
}) => {
const { app } = await request(
saleorApiUrl,
getAppWebhooksQueryDocument,
{
id: appId,
},
{ "Authorization-Bearer": token }
);
return app?.webhooks ?? [];
};

View file

@ -1,48 +0,0 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
export const AppDetailsFragment = graphql(/* GraphQL */ `
fragment AppDetails on App {
id
name
isActive
type
created
manifestUrl
}
`);
const getAppsQueryDocument = graphql(/* GraphQL */ `
query GetApps {
apps(
first: 100
filter: { type: THIRDPARTY }
sortBy: { field: CREATION_DATE, direction: DESC }
) {
totalCount
edges {
node {
...AppDetails
}
}
}
}
`);
export const getAppsListQuery = async ({
saleorApiUrl,
token,
}: {
saleorApiUrl: string;
token: string;
}) => {
const { apps } = await request(
saleorApiUrl,
getAppsQueryDocument,
{},
{ "Authorization-Bearer": token }
);
return apps?.edges.map(({ node }) => node) ?? [];
};

View file

@ -1,58 +0,0 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const installAppMutationDocument = graphql(/* GraphQL */ `
mutation InstallApp($input: AppInstallInput!) {
appInstall(input: $input) {
appInstallation {
id
status
appName
}
errors {
field
message
}
}
}
`);
export const installAppMutation = async ({
saleorApiUrl,
token,
appName,
manifestUrl,
}: {
saleorApiUrl: string;
token: string;
manifestUrl: string;
appName: string;
}) => {
const { appInstall } = await request(
saleorApiUrl,
installAppMutationDocument,
{
input: {
manifestUrl,
activateAfterInstallation: true,
appName,
},
},
{ "Authorization-Bearer": token }
);
if (appInstall?.errors.length) {
console.log("Sth went wrong", appInstall.errors);
throw new Error(`Install app ${appName} mutation failed`);
}
if (!appInstall?.appInstallation) {
console.log("App installation not returned");
throw new Error(
`Install app ${appName} mutation failed - no app installation data in the response`
);
}
return appInstall?.appInstallation;
};

View file

@ -1,40 +0,0 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const removeWebhookMutationDocument = graphql(/* GraphQL */ `
mutation RemoveWebhook($webhookId: ID!) {
webhookDelete(id: $webhookId) {
errors {
field
message
}
}
}
`);
export const removeWebhookMutation = async ({
saleorApiUrl,
token,
webhookId,
}: {
saleorApiUrl: string;
token: string;
webhookId: string;
}) => {
const { webhookDelete } = await request(
saleorApiUrl,
removeWebhookMutationDocument,
{
webhookId,
},
{ "Authorization-Bearer": token }
);
if (webhookDelete?.errors.length) {
console.log("Sth went wrong", webhookDelete.errors);
throw new Error(`Remove webhook mutation failed`);
}
return;
};

View file

@ -1,48 +0,0 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
import { AppErrorCode } from "../generated/graphql";
const uninstallAppMutationDocument = graphql(/* GraphQL */ `
mutation UninstallApp($id: ID!) {
appDelete(id: $id) {
errors {
field
message
code
}
}
}
`);
export const uninstallAppMutation = async ({
saleorApiUrl,
token,
id,
}: {
saleorApiUrl: string;
token: string;
id: string;
}) => {
const { appDelete } = await request(
saleorApiUrl,
uninstallAppMutationDocument,
{
id,
},
{ "Authorization-Bearer": token }
);
if (appDelete?.errors.length) {
const error = appDelete.errors[0];
if (error.code === AppErrorCode.NotFound) {
throw new Error(`Uninstall app ${id} mutation failed - no installed app with this ID`);
}
throw new Error(
`Uninstall app ${id} mutation failed. API responded with error: ${error.code} - ${error.message}`
);
}
return;
};

View file

@ -1,97 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "ES2022" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./build" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
"useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */,
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -1,17 +0,0 @@
{
"extends": [
"//"
],
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"env": [
"APP_DEBUG",
"NODE_ENV",
"INSTANCE_URL",
"USER_EMAIL",
"USER_PASSWORD"
]
}
}
}

18
apps/cms-v2/.env.example Normal file
View file

@ -0,0 +1,18 @@
# The key used for metadata encryption. Required for production builds
SECRET_KEY=
# APL Config
# https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md
APL=file
REST_APL_ENDPOINT=
REST_APL_TOKEN=
APP_LOG_LEVEL=info
# Local development variables. When developped locally with Saleor inside docker, these can be set to:
# APP_IFRAME_BASE_URL = http://localhost:3000, so Dashboard on host can access iframe
# APP_API_BASE_URL=http://host.docker.internal:3000 - so Saleor can reach App running on host, from the container.
# 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
APP_IFRAME_BASE_URL=
APP_API_BASE_URL=

121
apps/cms-v2/CHANGELOG.md Normal file
View file

@ -0,0 +1,121 @@
# saleor-app-cms-v2
## 2.3.3
### Patch Changes
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
- 5dee65a: Updated dependencies:
- @graphql-codegen/cli@5.0.0
- 2e29699: Updated Sentry package
## 2.3.2
### Patch Changes
- e8660e8: Extracted UI components and use shared package
- 30140ee: Improved some text typos.
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- Updated dependencies [e8660e8]
- @saleor/apps-shared@1.9.0
- @saleor/apps-ui@1.2.0
## 2.3.1
### Patch Changes
- a81f061: Updated Macaw to pre-127
- d9d0f64: Fixed background color of the modal. Previously white background was missing and only blur was applied. Now it is blurred and white with an opacity (in the dark mode it is fixed with dark grey)
- fcc37e7: Remove clsx package from the projects no longer using it.
- Updated dependencies [2a1385b]
- Updated dependencies [a81f061]
- Updated dependencies [fcc37e7]
- @saleor/apps-shared@1.8.1
- @saleor/react-hook-form-macaw@0.2.5
- @saleor/apps-ui@1.1.8
## 2.3.0
### Minor Changes
- 6f1c5c9: Added Payload CMS support.
### Patch Changes
- e9378e7: Fix PayloadCMS form where two inputs were stuck together without a margin. Now, they are placed in two columns
- 6f1c5c9: Fix styling of modal in the dark mode
## 2.2.2
### Patch Changes
- 8b3d961: Updated Macaw UI to pre.118
- c50797e: Extracted MetadataManager creation to factory from shared package
- Updated dependencies [8b3d961]
- Updated dependencies [c50797e]
- @saleor/react-hook-form-macaw@0.2.4
- @saleor/apps-shared@1.8.0
- @saleor/apps-ui@1.1.7
## 2.2.1
### Patch Changes
- 3002354: Added error logging for exceptions thrown at tRPC routes.
## 2.2.0
### Minor Changes
- fe767a4: Changed public (manifest) name app to be "CMS" (previously it was "CMS 2", since two apps existed at the same time)
## 2.1.2
### Patch Changes
- 261957f: Updated dependencies: vite, vitest, eslint, prettier, dotenv, turbo, syncpack, changesets, lint staged
- Updated dependencies [261957f]
- @saleor/react-hook-form-macaw@0.2.3
- @saleor/apps-shared@1.7.6
- @saleor/apps-ui@1.1.6
## 2.1.1
### Patch Changes
- 2fab86b: Updated graphql package to 16.7.1 and @graphql-codegen related dependencies to the latest version.
- aa6fec1: Updated Macaw UI to pre-106
- 4578659: Made Builder.io api keys inputs type of "password" so they are masked now
- Updated dependencies [aa6fec1]
- Updated dependencies [aa6fec1]
- @saleor/react-hook-form-macaw@0.2.2
- @saleor/apps-shared@1.7.5
- @saleor/apps-ui@1.1.5
## 2.1.0
### Minor Changes
- 5d41af9: Added validation for channel-provider connection. Now form will display error when user tries to add a connection that already exists.
- 5d41af9: Added set of improvements around app quality
- Ensured forms have fields properly set as "required", so form validation will prevent empty form submissions
- Contentful and DatoCMS forms now validate the credentials.
- Added logs (server side) in various places
- Bulk sync finished now triggers notification
- 5d41af9: Added skeletons instead raw "Loading" texts. Also removed unnecessary warning message in bulk sync section
### Patch Changes
- Updated dependencies [70cb741]
- Updated dependencies [e7c2d3a]
- Updated dependencies [3c6cd4c]
- Updated dependencies [6210447]
- @saleor/react-hook-form-macaw@0.2.1
- @saleor/apps-shared@1.7.4
- @saleor/apps-ui@1.1.4

View file

@ -0,0 +1,19 @@
fragment BulkImportProduct on Product {
id
name
slug
variants {
id
name
channelListings {
channel {
id
slug
}
price {
amount
currency
}
}
}
}

View file

@ -2,23 +2,17 @@ fragment WebhookProduct on Product {
id id
name name
slug slug
media {
url
}
channelListings { channelListings {
id id
channel { channel {
id id
slug slug
} }
isPublished
} }
variants { variants {
id id
name name
sku
channelListings { channelListings {
id
channel { channel {
id id
slug slug
@ -28,9 +22,5 @@ fragment WebhookProduct on Product {
currency currency
} }
} }
metadata {
key
value
}
} }
} }

View file

@ -1,25 +1,12 @@
fragment WebhookProductVariant on ProductVariant { fragment WebhookProductVariant on ProductVariant {
id id
name name
sku
product { product {
id id
name name
slug slug
media {
url
} }
channelListings { channelListings {
id
channel {
id
slug
}
isPublished
}
}
channelListings {
id
channel { channel {
id id
slug slug
@ -29,8 +16,4 @@ fragment WebhookProductVariant on ProductVariant {
currency currency
} }
} }
metadata {
key
value
}
} }

View file

@ -0,0 +1,7 @@
query FetchChannels {
channels {
id
slug
name
}
}

View file

@ -0,0 +1,13 @@
query FetchProductsPaginated($channel: String, $after: String) {
products(first: 100, channel: $channel, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
...BulkImportProduct
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,29 @@
const { z } = require("zod");
const { withSentryConfig } = require("@sentry/nextjs"); const { withSentryConfig } = require("@sentry/nextjs");
const RequiredEnvs = z.object({
APL: z.string().min(1),
});
/** @type {import('next').NextConfig} */
const nextConfig = () => {
try {
RequiredEnvs.parse(process.env);
} catch (e) {
console.error("🚫 Missing required env variables, see message below");
console.error(e.issues);
process.exit(1);
}
return {
reactStrictMode: true,
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
};
};
const isSentryPropertiesInEnvironment = const isSentryPropertiesInEnvironment =
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_PROJECT && process.env.SENTRY_ORG; process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_PROJECT && process.env.SENTRY_ORG;
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["@saleor/apps-shared"],
};
const configWithSentry = withSentryConfig( const configWithSentry = withSentryConfig(
nextConfig, nextConfig,
{ {

72
apps/cms-v2/package.json Normal file
View file

@ -0,0 +1,72 @@
{
"name": "saleor-app-cms-v2",
"version": "2.3.3",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
"fetch-schema": "curl https://raw.githubusercontent.com/saleor/saleor/${npm_package_saleor_schemaVersion}/saleor/graphql/schema.graphql > graphql/schema.graphql",
"generate": "graphql-codegen",
"lint": "next lint",
"lint:fix": "eslint --fix .",
"start": "next start",
"test": "vitest"
},
"dependencies": {
"@datocms/cma-client-browser": "2.0.0",
"@hookform/resolvers": "^3.3.1",
"@saleor/app-sdk": "0.43.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@saleor/react-hook-form-macaw": "workspace:*",
"@sentry/nextjs": "7.67.0",
"@tanstack/react-query": "^4.29.19",
"@trpc/client": "10.38.1",
"@trpc/next": "10.38.1",
"@trpc/react-query": "10.38.1",
"@trpc/server": "10.38.1",
"@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.4",
"contentful-management": "10.38.3",
"graphql": "16.7.1",
"graphql-tag": "^2.12.6",
"jsdom": "^20.0.3",
"next": "13.4.8",
"p-ratelimit": "1.0.1",
"pino": "^8.14.1",
"pino-pretty": "^10.0.0",
"qs": "6.11.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "4.0.10",
"react-hook-form": "^7.43.9",
"strapi-sdk-js": "2.2.0",
"urql": "^4.0.4",
"usehooks-ts": "^2.9.1",
"vite": "4.4.8",
"vitest": "0.34.1",
"zod": "3.21.4"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/qs": "^6.9.7",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"eslint": "8.46.0",
"eslint-config-saleor": "workspace:*",
"node-mocks-http": "^1.12.2",
"typescript": "5.1.6"
},
"private": true,
"saleor": {
"schemaVersion": "3.10"
}
}

View file

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -0,0 +1,14 @@
import { BulkImportProductFragment } from "../../../generated/graphql";
export type BulkSyncProcessorHooks = {
onUploadStart?: (context: { variantId: string }) => void;
onUploadSuccess?: (context: { variantId: string }) => void;
onUploadError?: (context: { variantId: string; error: Error }) => void;
};
export interface BulkSyncProcessor {
uploadProducts(
products: BulkImportProductFragment[],
hooks: BulkSyncProcessorHooks
): Promise<void>;
}

View file

@ -0,0 +1,87 @@
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { trpcClient } from "../trpc/trpc-client";
import { useForm } from "react-hook-form";
import { Select } from "@saleor/react-hook-form-macaw";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { ProvidersResolver } from "../providers/providers-resolver";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
const FormSchema = z.object({
connectionId: z.string().min(7),
});
const EmptyState = () => (
<Layout.AppSectionCard>
<Box display="flex" flexDirection={"column"} gap={4} justifyContent={"center"}>
<Text variant="heading">Bulk products synchronization</Text>
<Text>Create a channel connection above to enable bulk synchronization.</Text>
</Box>
</Layout.AppSectionCard>
);
export const BulkSyncSection = () => {
const { push } = useRouter();
const { data: connections } = trpcClient.channelsProvidersConnection.fetchConnections.useQuery();
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
const { control, handleSubmit } = useForm({
defaultValues: {
connectionId: "",
},
resolver: zodResolver(FormSchema),
});
if (!connections || !providers) {
return <SkeletonLayout.Section />;
}
if (connections.length === 0) {
return <EmptyState />;
}
return (
<Layout.AppSectionCard>
<Text as="h2" marginBottom={6} variant="heading">
Bulk products synchronization
</Text>
<Text as="p">
Choose a connection and start synchronization. Process is running in the browser.
</Text>
<Text as="p" variant="bodyStrong">
Do not close the app until it is finished
</Text>
<Box
display="grid"
gap={4}
marginTop={4}
as="form"
onSubmit={handleSubmit((values) => {
push(`/bulk-sync/${values.connectionId}`);
})}
>
<Select
required
control={control}
name="connectionId"
label="Connection"
options={connections.map((c) => {
const provider = providers.find((p) => p.id === c.providerId)!;
const providerDisplay = ProvidersResolver.createProviderMeta(provider.type);
return {
label: `${c.channelSlug} -> ${provider?.configName} (${providerDisplay.displayName})`,
value: c.id,
};
})}
/>
<ButtonsBox>
<Button type="submit">Start sync</Button>
</ButtonsBox>
</Box>
</Layout.AppSectionCard>
);
};

View file

@ -0,0 +1,184 @@
import { Breadcrumbs, ButtonsBox, Layout } from "@saleor/apps-ui";
import { ArrowRightIcon, Box, Button, Text } from "@saleor/macaw-ui/next";
import { useEffect, useRef, useState } from "react";
import { ChannelProviderConnectionConfig, ProvidersConfig } from "../configuration";
import { AppHeader } from "../ui/app-header";
import { useBulkSyncProductsState } from "./use-bulk-sync-products-state";
import { useFetchAllProducts } from "./use-fetch-all-products";
import { VariantsSyncStatusList } from "./variants-sync-status-list";
import { ProvidersResolver } from "../providers/providers-resolver";
import { useDashboardNotification } from "@saleor/apps-shared";
const FetchProductsStep = (props: { onButtonClick(): void }) => {
return (
<Layout.AppSectionCard
footer={
<ButtonsBox>
<Button onClick={props.onButtonClick}>Prefetch products</Button>
</ButtonsBox>
}
>
<Text variant="heading" as="h2" marginBottom={4}>
Saleor products fetch
</Text>
<Text as="p">Click the button to start fetching products from Saleor API</Text>
<Text as="p">After products are fetched, you will be able to upload them to the CMS</Text>
</Layout.AppSectionCard>
);
};
const SaleorProductsFetchingStep = () => {
return (
<Box>
<Text variant="heading" as="h2" marginBottom={4}>
Saleor products fetch
</Text>
<Text as="p">Fetching...</Text>
</Box>
);
};
const SaleorProductsFetchedStep = (props: { productsNo: number; variantsNo: number }) => {
return (
<Box>
<Text variant="heading" as="h2" marginBottom={4}>
Saleor products fetch
</Text>
<Text as="p">
Fetched {props.productsNo} products and {props.variantsNo} variants
</Text>
</Box>
);
};
type Status = "initial" | "fetching" | "fetched" | "uploading";
export const BulkSyncView = ({
configuration,
connection,
}: {
configuration: ProvidersConfig.AnyFullShape;
connection: ChannelProviderConnectionConfig.FullShape;
}) => {
const processor = useRef(ProvidersResolver.createBulkSyncProcessor(configuration));
const [state, setState] = useState<Status>("initial");
const { notifySuccess } = useDashboardNotification();
const { products, finished: saleorProductsFetchFinished } = useFetchAllProducts(
state === "fetching",
connection.channelSlug,
);
const { productsStatusList, setInitialProducts, setItemStatus, finished } =
useBulkSyncProductsState();
useEffect(() => {
if (finished) {
notifySuccess("Bulk sync ended", "All products have been synced, please verify results");
}
}, [finished, notifySuccess]);
useEffect(() => {
if (!saleorProductsFetchFinished) {
return;
}
setInitialProducts(products);
setState("fetched");
}, [products, saleorProductsFetchFinished, setInitialProducts]);
useEffect(() => {
if (state !== "uploading") {
return;
}
processor.current.uploadProducts(products, {
onUploadStart({ variantId }) {
setItemStatus(variantId, "uploading");
},
onUploadSuccess({ variantId }) {
setItemStatus(variantId, "success");
},
onUploadError({ error, variantId }) {
// User will be notified about the error in the UI
setItemStatus(variantId, "error");
},
});
}, [state, products, configuration, setItemStatus]);
return (
<Box>
<AppHeader
text="Perform bulk products sync between Saleor and your CMS"
breadcrumbs={[
<Breadcrumbs.Item key="bulk-sync">Bulk Sync</Breadcrumbs.Item>,
<Breadcrumbs.Item key="connection-name">
<Box display="flex" gap={2} alignItems="center">
{connection.channelSlug}
<ArrowRightIcon /> {configuration.configName}
</Box>
</Breadcrumbs.Item>,
]}
/>
<Layout.AppSection
marginBottom={8}
heading="1. Fetch products"
sideContent={
<Text>First pre-fetch all Product Variants from Saleor. Do not close the app.</Text>
}
>
{(() => {
switch (state) {
case "initial": {
return (
<FetchProductsStep
onButtonClick={() => {
setState("fetching");
}}
/>
);
}
case "fetching": {
return <SaleorProductsFetchingStep />;
}
case "fetched":
case "uploading": {
return (
<SaleorProductsFetchedStep
productsNo={products.length}
variantsNo={productsStatusList?.length ?? 0}
/>
);
}
}
})()}
</Layout.AppSection>
{(state === "fetched" || state === "uploading") && productsStatusList && (
<Layout.AppSection
marginTop={14}
heading="2. Upload to the CMS"
sideContent={<Text>Send listed variants to the CMS</Text>}
>
<Layout.AppSectionCard>
<Text as="h2" marginBottom={4} variant="heading">
Upload products
</Text>
{state === "fetched" && (
<Box marginBottom={4}>
<Text as="p" marginBottom={2}>
Verify products below and click the button to start uploading.
</Text>
<Button onClick={() => setState("uploading")}>Start uploading</Button>
</Box>
)}
<VariantsSyncStatusList marginTop={8} variants={productsStatusList} />
</Layout.AppSectionCard>
</Layout.AppSection>
)}
</Box>
);
};

View file

@ -0,0 +1,54 @@
import { useCallback, useEffect, useState } from "react";
import { BulkImportProductFragment } from "../../../generated/graphql";
import { VariantsSyncStatusListItem } from "./variants-sync-status-list";
export const useBulkSyncProductsState = () => {
const [finished, setFinished] = useState(false);
const [productsStatusList, setProductsStatusList] = useState<VariantsSyncStatusListItem[] | null>(
null
);
useEffect(() => {
if (productsStatusList?.every((item) => item.status === "success" || item.status === "error")) {
setFinished(true);
}
}, [productsStatusList]);
return {
finished,
productsStatusList,
setInitialProducts: useCallback((products: BulkImportProductFragment[]) => {
setProductsStatusList(
products.flatMap((p) => {
const items: VariantsSyncStatusListItem[] =
p.variants?.map((v) => ({
productID: p.id,
productName: p.name,
status: "pending",
variantId: v.id,
variantName: v.name,
})) ?? [];
return items;
})
);
}, []),
setItemStatus: useCallback(
(variantID: string, status: VariantsSyncStatusListItem["status"]) => {
setProductsStatusList((items) =>
items!.map((item) => {
if (item.variantId === variantID) {
return {
...item,
status: status,
};
}
return item;
})
);
},
[]
),
};
};

View file

@ -0,0 +1,65 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useEffect, useState } from "react";
import { createGraphQLClient } from "@saleor/apps-shared";
import {
BulkImportProductFragment,
FetchProductsPaginatedDocument,
} from "../../../generated/graphql";
/**
* Original source - apps/search
*/
export const useFetchAllProducts = (started: boolean, channelSlug: string) => {
const { appBridgeState } = useAppBridge();
const saleorApiUrl = appBridgeState?.saleorApiUrl!;
const [products, setProducts] = useState<BulkImportProductFragment[]>([]);
const [finished, setFinished] = useState(false);
useEffect(() => {
if (!started) {
return;
}
if (!appBridgeState?.token) {
return;
}
const token = appBridgeState.token;
const client = createGraphQLClient({ saleorApiUrl, token });
if (!client) {
return;
}
const getProducts = async (cursor?: string): Promise<void> => {
const response = await client
.query(FetchProductsPaginatedDocument, {
after: cursor,
channel: channelSlug,
})
.toPromise();
const newProducts = response?.data?.products?.edges.map((e) => e.node) ?? [];
if (newProducts.length > 0) {
setProducts((ps) => [...ps, ...newProducts]);
}
if (
response?.data?.products?.pageInfo.hasNextPage &&
response?.data?.products?.pageInfo.endCursor
) {
return getProducts(response.data.products?.pageInfo.endCursor);
} else {
setFinished(true);
return;
}
};
getProducts(undefined);
}, [appBridgeState?.token, saleorApiUrl, started, channelSlug]);
return { products, finished };
};

View file

@ -0,0 +1,75 @@
import { SemanticChip } from "@saleor/apps-ui";
import { Box, BoxProps, Text } from "@saleor/macaw-ui/next";
export type VariantsSyncStatusListItem = {
productID: string;
productName: string;
variantName: string;
variantId: string;
status: "pending" | "uploading" | "success" | "error";
};
type Props = {
variants: Array<VariantsSyncStatusListItem>;
} & BoxProps;
const Row = (props: BoxProps) => (
<Box
padding={2}
display="grid"
__gridTemplateColumns={"2fr 2fr 1fr"}
gap={4}
alignItems="center"
{...props}
/>
);
export const VariantsSyncStatusList = ({ variants, ...props }: Props) => {
const renderStatus = (status: VariantsSyncStatusListItem["status"]) => {
switch (status) {
case "pending": {
return null;
}
case "success": {
return <SemanticChip variant="success">Uploaded</SemanticChip>;
}
case "error": {
return <SemanticChip variant="error">Error</SemanticChip>;
}
case "uploading": {
return <SemanticChip variant="default">Uploading</SemanticChip>;
}
}
};
return (
<Box {...props}>
<Row>
<Text variant="caption">Product</Text>
<Text variant="caption">Variant</Text>
<Text variant="caption">Status</Text>
</Row>
{variants.map((variant) => (
<Row key={variant.variantId}>
<Box>
<Text variant="bodyStrong" as="p">
{variant.productName}
</Text>
<Text size="small" as="p">
{variant.productID}
</Text>
</Box>
<Box>
<Text variant="bodyStrong" as="p">
{variant.variantName}
</Text>
<Text size="small" as="p">
{variant.variantId}
</Text>
</Box>
<Box>{renderStatus(variant.status)}</Box>
</Row>
))}
</Box>
);
};

View file

@ -0,0 +1,89 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightIcon, Box, Text } from "@saleor/macaw-ui/next";
import { Select } from "@saleor/react-hook-form-macaw";
import { useForm } from "react-hook-form";
import { ChannelProviderConnectionConfig } from "../configuration";
import { trpcClient } from "../trpc/trpc-client";
import { TRPCError } from "@trpc/server";
export type AddConnectionFormSchema = Omit<
ChannelProviderConnectionConfig.InputShape,
"providerType"
>;
export const AddConnectionFormID = "new-connection-form";
export const AddConnectionForm = (props: {
defaultValues: AddConnectionFormSchema;
onSubmit(values: AddConnectionFormSchema): Promise<void>;
}) => {
const { data: channels } = trpcClient.channelsProvidersConnection.fetchAllChannels.useQuery();
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
const {
handleSubmit,
control,
setError,
formState: { errors },
} = useForm({
defaultValues: props.defaultValues,
resolver: zodResolver(
ChannelProviderConnectionConfig.Schema.Input.omit({ providerType: true })
),
});
const onSubmit = (values: AddConnectionFormSchema) => {
props.onSubmit(values).catch((err: TRPCError) => {
setError("channelSlug", {
type: "manual",
});
setError("providerId", {
type: "manual",
});
setError("root.serverError", {
type: "manual",
message: err.message,
});
});
};
return (
<Box onSubmit={handleSubmit(onSubmit)} as="form" id={AddConnectionFormID}>
<Box display="grid" __gridTemplateColumns="1fr auto 1fr" gap={4} alignItems="center">
<Select
required
size="small"
control={control}
name="channelSlug"
label="Channel"
options={
channels?.map((c) => ({
value: c.slug,
label: c.name,
})) ?? []
}
/>
<ArrowRightIcon />
<Select
required
size="small"
control={control}
name="providerId"
label="Provider"
options={
providers?.map((p) => ({
value: p.id,
label: p.configName,
})) ?? []
}
/>
</Box>
{errors.root?.serverError && (
<Text as="p" marginTop={2} color="textCriticalSubdued">
{errors.root?.serverError.message}
</Text>
)}
</Box>
);
};

View file

@ -0,0 +1,66 @@
import { Button, Text } from "@saleor/macaw-ui/next";
import { Modal } from "../ui/modal";
import {
AddConnectionForm,
AddConnectionFormID,
AddConnectionFormSchema,
} from "./add-connection-form";
import { trpcClient } from "../trpc/trpc-client";
import { ButtonsBox, SkeletonLayout } from "@saleor/apps-ui";
const defaultValues: AddConnectionFormSchema = { channelSlug: "", providerId: "" };
export const AddConnectionModal = (props: { onSuccess(): void; onClose(): void }) => {
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
if (!providers) {
return <SkeletonLayout.Section />;
}
const { mutateAsync: addProviderMutate, isLoading } =
trpcClient.channelsProvidersConnection.addConnection.useMutation({
onSuccess() {
props.onSuccess();
},
});
const handleFormSubmit = async (values: AddConnectionFormSchema) => {
const providerType = providers.find((p) => p.id === values.providerId)?.type;
if (!providerType) {
throw new Error("Provider not found");
}
return addProviderMutate({
...values,
providerType,
});
};
return (
<Modal onClose={props.onClose}>
<Text as="h2" variant="heading">
Connect channel with Provider
</Text>
<Text as="p" marginBottom={6}>
Once connected, operations on product variants on this channel will be sent to selected CMS
platform.
</Text>
<AddConnectionForm onSubmit={handleFormSubmit} defaultValues={defaultValues} />
<ButtonsBox marginTop={8}>
<Button
disabled={isLoading}
variant="tertiary"
onClick={() => {
props.onClose();
}}
>
Close
</Button>
<Button variant="primary" type="submit" form={AddConnectionFormID}>
Add connection
</Button>
</ButtonsBox>
</Modal>
);
};

View file

@ -0,0 +1,71 @@
import { AppConfigMetadataManager } from "@/modules/configuration/app-config-metadata-manager";
import { createSettingsManager } from "@/modules/configuration/metadata-manager";
import { ChannelProviderConnectionConfig } from "@/modules/configuration/schemas/channel-provider-connection.schema";
import { protectedClientProcedure } from "@/modules/trpc/protected-client-procedure";
import { router } from "@/modules/trpc/trpc-server";
import { z } from "zod";
import { FetchChannelsDocument } from "../../../generated/graphql";
import { TRPCError } from "@trpc/server";
const procedure = protectedClientProcedure.use(({ ctx, next }) => {
const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId!);
return next({
ctx: {
appConfigService: new AppConfigMetadataManager(settingsManager),
},
});
});
export const channelProviderConnectionRouter = router({
fetchAllChannels: protectedClientProcedure.query(async ({ ctx }) => {
const channels = await ctx.apiClient.query(FetchChannelsDocument, {});
return channels.data?.channels ?? [];
}),
fetchConnections: procedure.query(async ({ ctx }) => {
return (await ctx.appConfigService.get()).connections.getConnections();
}),
fetchConnection: procedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => {
return (await ctx.appConfigService.get()).connections.getConnectionById(input.id) ?? null;
}),
addConnection: procedure
.input(ChannelProviderConnectionConfig.Schema.Input)
.mutation(async ({ ctx, input }) => {
const config = await ctx.appConfigService.get();
try {
config.connections.addConnection(input);
} catch (e) {
switch ((e as { cause: string }).cause) {
case "PROVIDER_DOESNT_EXIST":
throw new TRPCError({
code: "BAD_REQUEST",
cause: "PROVIDER_DOESNT_EXIST",
message: "Provider doesnt exist",
});
case "CONNECTION_ALREADY_EXISTS":
throw new TRPCError({
code: "CONFLICT",
cause: "CONNECTION_EXISTS",
message: "Connection already exists",
});
}
}
ctx.appConfigService.set(config);
}),
removeConnection: procedure
.input(
z.object({
id: z.string(),
})
)
.mutation(async ({ ctx, input }) => {
const config = await ctx.appConfigService.get();
config.connections.deleteConnection(input.id);
ctx.appConfigService.set(config);
}),
});

View file

@ -0,0 +1,7 @@
import { Text } from "@saleor/macaw-ui/next";
export const ChanelProviderConnectionsSectionHeader = () => (
<Text marginBottom={4} as="h2" variant="heading">
Channels Connections
</Text>
);

View file

@ -0,0 +1,94 @@
import { useDashboardNotification } from "@saleor/apps-shared";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { useState } from "react";
import { trpcClient } from "../trpc/trpc-client";
import { AddConnectionModal } from "./add-connection-modal";
import { ChanelProviderConnectionsSectionHeader } from "./channel-provider-connections-section-header";
import { ConnectionsList } from "./connections-list";
const NoConnections = (props: { onCreate(): void; enabled: boolean }) => (
<Box>
<ChanelProviderConnectionsSectionHeader />
<Text as="p">
No channels connected yet.{" "}
{!props.enabled &&
"Ensure you have created a provider configuration that can be connected first."}
</Text>
</Box>
);
export const ChannelProviderConnectionList = () => {
const [dialogOpen, setDialogOpen] = useState(false);
const {
data: connectionsData,
refetch: refetchConnections,
isLoading,
} = trpcClient.channelsProvidersConnection.fetchConnections.useQuery();
const { mutate: removeConnection } =
trpcClient.channelsProvidersConnection.removeConnection.useMutation({
onSuccess() {
refetchConnections();
notifySuccess("Success", "Removed connection");
},
});
const { notifySuccess } = useDashboardNotification();
// Prefetch
trpcClient.channelsProvidersConnection.fetchAllChannels.useQuery();
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
if (!providers) {
return <SkeletonLayout.Section />;
}
const handleDelete = (connectionId: string) => {
removeConnection({ id: connectionId });
};
if (isLoading || !connectionsData) {
return <SkeletonLayout.Section />;
}
return (
<Layout.AppSectionCard
footer={
providers.length > 0 && (
<ButtonsBox>
<Button
onClick={() => {
setDialogOpen(true);
}}
>
Add connection
</Button>
</ButtonsBox>
)
}
>
{dialogOpen && (
<AddConnectionModal
onClose={() => {
setDialogOpen(false);
}}
onSuccess={() => {
refetchConnections();
notifySuccess("Success", "Connection created");
setDialogOpen(false);
}}
/>
)}
{connectionsData.length === 0 && (
<NoConnections
enabled={providers.length > 0}
onCreate={() => {
setDialogOpen(true);
}}
/>
)}
{connectionsData.length > 0 && <ConnectionsList onRemove={handleDelete} />}
</Layout.AppSectionCard>
);
};

View file

@ -0,0 +1,56 @@
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { trpcClient } from "../trpc/trpc-client";
import { ChanelProviderConnectionsSectionHeader } from "./channel-provider-connections-section-header";
import { ProvidersResolver } from "../providers/providers-resolver";
import { SkeletonLayout } from "@saleor/apps-ui";
export const ConnectionsList = (props: { onRemove(connectionId: string): void }) => {
const { data } = trpcClient.channelsProvidersConnection.fetchConnections.useQuery();
const { data: channels } = trpcClient.channelsProvidersConnection.fetchAllChannels.useQuery();
const { data: providers } = trpcClient.providersConfigs.getAll.useQuery();
if (!data || !providers) {
return <SkeletonLayout.Section />;
}
return (
<Box>
<ChanelProviderConnectionsSectionHeader />
<Box
display="grid"
justifyContent={"space-between"}
__gridTemplateColumns={"1fr 1fr auto"}
gap={4}
alignItems="center"
>
<Text variant="caption">Saleor Channel</Text>
<Text variant="caption">Target CMS</Text>
<div />
{data?.map((conn) => {
const provider = providers.find((p) => p.id === conn.providerId);
if (!provider) {
throw new Error("Provider not found");
}
const providerName = ProvidersResolver.createProviderMeta(provider.type).displayName;
return (
<React.Fragment key={conn.id}>
<Text>{channels?.find((c) => c.slug === conn.channelSlug)?.name}</Text>
<Text>
<Text>{provider.configName}</Text>
<Text color="textNeutralSubdued"> ({providerName})</Text>
</Text>
<Button onClick={() => props.onRemove(conn.id)} variant="tertiary">
Remove
</Button>
</React.Fragment>
);
})}
</Box>
</Box>
);
};

View file

@ -0,0 +1,36 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { AppConfig } from "./app-config";
import { createSettingsManager } from "./metadata-manager";
import { createGraphQLClient } from "@saleor/apps-shared";
import { AuthData } from "@saleor/app-sdk/APL";
export class AppConfigMetadataManager {
public readonly metadataKey = "app-config-v1";
constructor(private mm: SettingsManager) {}
async get() {
const metadata = await this.mm.get(this.metadataKey);
return metadata ? AppConfig.parse(metadata) : new AppConfig();
}
set(config: AppConfig) {
return this.mm.set({
key: this.metadataKey,
value: config.serialize(),
});
}
static createFromAuthData(authData: AuthData): AppConfigMetadataManager {
const settingsManager = createSettingsManager(
createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
}),
authData.appId
);
return new AppConfigMetadataManager(settingsManager);
}
}

View file

@ -0,0 +1,148 @@
import { describe, it, expect, beforeEach } from "vitest";
import { AppConfig } from "./app-config";
import { ContentfulProviderConfig } from "./schemas/contentful-provider.schema";
import { DatocmsProviderConfig } from "./schemas/datocms-provider.schema";
const getMockContentfulInput = (): ContentfulProviderConfig.InputShape => {
return {
configName: "Test",
type: "contentful",
contentId: "test",
authToken: "test",
environment: "test",
productVariantFieldsMapping: {
channels: "channels",
productId: "productId",
productName: "productName",
productSlug: "productSlug",
variantId: "variantId",
variantName: "variantName",
},
spaceId: "test",
};
};
const getMockDatocmsInput = (): DatocmsProviderConfig.InputShape => {
return {
configName: "Test",
type: "datocms",
itemType: "test",
authToken: "test",
productVariantFieldsMapping: {
channels: "channels",
productId: "productId",
productName: "productName",
productSlug: "productSlug",
variantId: "variantId",
variantName: "variantName",
},
};
};
describe("App Config", () => {
let appConfig: AppConfig;
beforeEach(() => {
appConfig = new AppConfig();
});
it("Constructs with empty state", () => {
expect(appConfig.connections.getConnections()).toEqual([]);
expect(appConfig.providers.getProviders()).toEqual([]);
});
describe("Providers", () => {
it("Can retrieve provider by ID from the config", () => {
appConfig.providers.addProvider(getMockContentfulInput());
const provider = appConfig.providers.getProviders()[0];
expect(provider).toBeDefined();
expect(appConfig.providers.checkProviderExists(provider.id)).toBe(true);
expect(appConfig.providers.getProviderById(provider.id)).toBeDefined();
});
it("Can update the provider", () => {
appConfig.providers.addProvider(getMockContentfulInput());
const provider = appConfig.providers.getProviders()[0] as ContentfulProviderConfig.FullShape;
appConfig.providers.updateProvider({
...provider,
spaceId: "new-space-id",
});
const providerUpdated =
appConfig.providers.getProviders()[0] as ContentfulProviderConfig.FullShape;
expect(providerUpdated.spaceId).toBe("new-space-id");
});
it("Can delete provider", () => {
appConfig.providers.addProvider(getMockContentfulInput());
const provider = appConfig.providers.getProviders()[0];
expect(provider).toBeDefined();
appConfig.providers.deleteProvider(provider.id);
expect(appConfig.providers.getProviders()).toEqual([]);
});
});
describe("Connections", () => {
it("Throws if trying to add connection with provider that doesnt exist", () => {
expect(() => {
appConfig.connections.addConnection({
channelSlug: "test",
providerId: "asdasd123",
providerType: "contentful",
});
}).toThrow();
});
it("Can add connection with existing provider", () => {
appConfig.providers.addProvider(getMockContentfulInput());
appConfig.connections.addConnection({
channelSlug: "test",
providerId: appConfig.providers.getProviders()[0].id,
providerType: "contentful",
});
expect(appConfig.connections.getConnections().length).toBe(1);
});
it("Removes all connections that include passed provider", () => {
appConfig.providers.addProvider(getMockContentfulInput());
appConfig.providers.addProvider(getMockDatocmsInput());
appConfig.connections.addConnection({
channelSlug: "test",
providerId: appConfig.providers.getProviders()[0].id,
providerType: "contentful",
});
appConfig.connections.addConnection({
channelSlug: "test2",
providerId: appConfig.providers.getProviders()[0].id,
providerType: "contentful",
});
appConfig.connections.addConnection({
channelSlug: "test2",
providerId: appConfig.providers.getProviders()[1].id,
providerType: "datocms",
});
expect(appConfig.connections.getConnections().length).toBe(3);
appConfig.providers.deleteProvider(appConfig.providers.getProviders()[0].id);
const leftConnections = appConfig.connections.getConnections();
expect(leftConnections.length).toBe(1);
expect(leftConnections[0].providerType).toBe("datocms");
});
});
});

View file

@ -0,0 +1,148 @@
import { ProvidersResolver } from "../providers/providers-resolver";
import { generateId } from "../shared/generate-id";
import { ChannelProviderConnectionConfig } from "./schemas/channel-provider-connection.schema";
import { ProvidersConfig, RootConfig } from "./schemas/root-config.schema";
export const Errors = {
CONNECTION_ALREADY_EXISTS: "CONNECTION_ALREADY_EXISTS",
PROVIDER_DOESNT_EXIST: "PROVIDER_DOESNT_EXIST",
};
/**
* TODO
* - test
* - extract and delegate smaller configs?
*/
export class AppConfig {
private rootData: RootConfig.Shape = {
providers: [],
connections: [],
};
constructor(initialData?: RootConfig.Shape) {
if (initialData) {
this.rootData = RootConfig.Schema.parse(initialData);
}
}
static parse(serializedSchema: string) {
return new AppConfig(JSON.parse(serializedSchema));
}
serialize() {
return JSON.stringify(this.rootData);
}
providers = {
checkProviderExists: (id: string) => {
return !!this.rootData.providers.find((p) => p.id === id);
},
addProvider: (providerConfigInput: ProvidersConfig.AnyInputShape) => {
const inputSchema = ProvidersResolver.getProviderInputSchema(providerConfigInput.type);
const parsedConfig = inputSchema.parse(providerConfigInput);
this.rootData.providers.push({
...parsedConfig,
id: generateId(),
});
return this;
},
updateProvider: (providerConfig: ProvidersConfig.AnyFullShape) => {
const schema = ProvidersResolver.getProviderSchema(providerConfig.type);
const parsedConfig = schema.parse(providerConfig);
this.rootData.providers = this.rootData.providers.map((p) => {
if (p.id === parsedConfig.id) {
return parsedConfig;
} else {
return p;
}
});
},
deleteProvider: (id: string) => {
this.rootData.providers = this.rootData.providers.filter((p) => p.id !== id);
this.connections.deleteConnectionsWithProvider(id);
return this;
},
getProviders: () => {
return this.rootData.providers;
},
getProviderById: (id: string) => {
return this.providers.getProviders().find((p) => p.id === id);
},
};
connections = {
getConnections: () => {
return this.rootData.connections;
},
deleteConnection: (connectionID: string) => {
this.rootData.connections = this.rootData.connections.filter((c) => c.id !== connectionID);
return this;
},
addConnection: (input: ChannelProviderConnectionConfig.InputShape) => {
if (!this.providers.checkProviderExists(input.providerId)) {
const err = new Error("Provider doesnt exist");
err.cause = Errors.PROVIDER_DOESNT_EXIST;
throw err;
}
const parsed = ChannelProviderConnectionConfig.Schema.Input.parse(input);
if (this.checkConnectionExists(input)) {
const error = new Error("Connection already exists");
error.cause = Errors.CONNECTION_ALREADY_EXISTS;
throw error;
}
this.rootData.connections.push({
...parsed,
id: generateId(),
});
return this;
},
deleteConnectionsWithProvider: (providerId: string) => {
this.rootData.connections = this.rootData.connections.filter((conn) => {
return conn.providerId !== providerId;
});
},
getConnectionById: (id: string) => {
return this.connections.getConnections().find((c) => c.id === id);
},
};
/**
* Returns true if exists
*/
private checkConnectionExists(newConnection: ChannelProviderConnectionConfig.InputShape) {
/**
* Make array of strings so its easy to compare
*/
const existingConnectionsAsStrings = this.connections
.getConnections()
.map((connection) => `${connection.channelSlug}-${connection.providerId}`);
const newConnectionAsString = `${newConnection.channelSlug}-${newConnection.providerId}`;
return existingConnectionsAsStrings.includes(newConnectionAsString);
}
}

View file

@ -0,0 +1,13 @@
/**
* Re-export for convenient imports
*/
export * from "./app-config";
export * from "./app-config-metadata-manager";
export * from "./metadata-manager";
export * from "./schemas/channel-provider-connection.schema";
export * from "./schemas/contentful-provider.schema";
export * from "./schemas/builder-provider.schema";
export * from "./schemas/datocms-provider.schema";
export * from "./schemas/strapi-provider.schema";
export * from "./schemas/saleor-provider-fields-mapping.schema";
export * from "./schemas/root-config.schema";

View file

@ -0,0 +1,12 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared";
import { Client } from "urql";
export const createSettingsManager = (
client: Pick<Client, "query" | "mutation">,
appId: string,
): SettingsManager => {
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
return metadataManagerFactory.create(client, appId);
};

View file

@ -0,0 +1,24 @@
import { SaleorProviderFieldsMappingKey } from "./schemas/saleor-provider-fields-mapping.schema";
export const printSaleorProductFields = (fieldName: SaleorProviderFieldsMappingKey) => {
switch (fieldName) {
case "variantName": {
return "Variant Name";
}
case "channels": {
return "Channels";
}
case "productId": {
return "Product ID";
}
case "productName": {
return "Product Name";
}
case "productSlug": {
return "Product Slug";
}
case "variantId": {
return "Variant ID";
}
}
};

View file

@ -0,0 +1,26 @@
import { z } from "zod";
import { SaleorProviderFieldsMappingSchema } from "./saleor-provider-fields-mapping.schema";
import { BuilderIo } from "@/modules/providers/builder.io/builder-io";
const InputSchema = z.object({
type: z.literal(BuilderIo.type),
privateApiKey: z.string().min(1),
publicApiKey: z.string().min(1),
configName: z.string().min(1),
modelName: z.string().min(1),
productVariantFieldsMapping: SaleorProviderFieldsMappingSchema,
});
const FullSchema = InputSchema.extend({
id: z.string(),
});
export namespace BuilderIoProviderConfig {
export type InputShape = z.infer<typeof InputSchema>;
export type FullShape = z.infer<typeof FullSchema>;
export const Schema = {
Input: InputSchema,
Full: FullSchema,
};
}

View file

@ -0,0 +1,22 @@
import { cmsTypes } from "@/modules/providers/providers-registry";
import { z } from "zod";
const InputSchema = z.object({
channelSlug: z.string().min(1),
providerId: z.string().min(1),
providerType: z.enum(cmsTypes),
});
const FullSchema = InputSchema.extend({
id: z.string(),
});
export namespace ChannelProviderConnectionConfig {
export type InputShape = z.infer<typeof InputSchema>;
export type FullShape = z.infer<typeof FullSchema>;
export const Schema = {
Input: InputSchema,
Full: FullSchema,
};
}

View file

@ -0,0 +1,27 @@
import { z } from "zod";
import { Contentful } from "../../providers/contentful/contentful";
import { SaleorProviderFieldsMappingSchema } from "./saleor-provider-fields-mapping.schema";
const InputSchema = z.object({
type: z.literal(Contentful.type),
authToken: z.string().min(1),
spaceId: z.string().min(1),
environment: z.string().min(1),
configName: z.string().min(1),
contentId: z.string().min(1),
productVariantFieldsMapping: SaleorProviderFieldsMappingSchema,
});
const FullSchema = InputSchema.extend({
id: z.string(),
});
export namespace ContentfulProviderConfig {
export type InputShape = z.infer<typeof InputSchema>;
export type FullShape = z.infer<typeof FullSchema>;
export const Schema = {
Input: InputSchema,
Full: FullSchema,
};
}

View file

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

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

@ -0,0 +1,49 @@
import { z } from "zod";
import { ChannelProviderConnectionConfig } from "./channel-provider-connection.schema";
import { ContentfulProviderConfig } from "./contentful-provider.schema";
import { BuilderIoProviderConfig } from "./builder-provider.schema";
import { StrapiProviderConfig } from "./strapi-provider.schema";
import { DatocmsProviderConfig } from "./datocms-provider.schema";
import { PayloadCmsProviderConfig } from "./payloadcms-provider.schema";
export namespace ProvidersConfig {
const AnyFull = z.union([
/**
* Add more for each provider
*/
ContentfulProviderConfig.Schema.Full,
DatocmsProviderConfig.Schema.Full,
StrapiProviderConfig.Schema.Full,
BuilderIoProviderConfig.Schema.Full,
PayloadCmsProviderConfig.Schema.Full,
]);
export const Schema = {
AnyFull: AnyFull,
AnyInput: z.union([
ContentfulProviderConfig.Schema.Input,
DatocmsProviderConfig.Schema.Input,
StrapiProviderConfig.Schema.Input,
BuilderIoProviderConfig.Schema.Input,
PayloadCmsProviderConfig.Schema.Input,
]),
AnyFullList: z.array(AnyFull),
};
export type AnyFullShape = z.infer<typeof Schema.AnyFull>;
export type AnyInputShape = z.infer<typeof Schema.AnyInput>;
}
export namespace RootConfig {
/**
* Store entire app config in single file
* - Only one request
* - Always transactional
*/
export const Schema = z.object({
providers: ProvidersConfig.Schema.AnyFullList,
connections: z.array(ChannelProviderConnectionConfig.Schema.Full),
});
export type Shape = z.infer<typeof Schema>;
}

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