From 1cb6e8b5fc2f32d1aa8e1a1d0e84062a29d8d565 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Mon, 3 Jul 2023 14:50:53 +0200 Subject: [PATCH] Manual app permissions setting by the user (#3829) * Remove react-markdown, render app about section as string * Add modal with permissions - WIP * Extract components * add permissions diff * add mutation * add notification * fix dialog scrolling * Extract messages in permissions dialog * test for useGetAvailableAppPermissions.ts * add test to state * fix enums * add changeset and extract root messages * Update wicked-berries-watch.md * fix linter * fix ts * cr fixes --- .changeset/rude-foxes-drop.md | 5 + .changeset/wicked-berries-watch.md | 5 + locale/defaultMessages.json | 45 ++ package-lock.json | 578 +----------------- package.json | 1 - .../components/AppDetailsPage/AboutCard.tsx | 5 +- .../AppDetailsPage/AppDetailsPage.tsx | 1 + .../AppDetailsPage/PermissionsCard.tsx | 49 +- .../components/AppDetailsPage/messages.ts | 4 + .../AppPermissionsDialog.tsx | 149 +++++ .../AppPermissionsDialogConfirmation.tsx | 77 +++ .../AppPermissionsDialogPermissionPicker.tsx | 80 +++ .../AppPermissionsDialogState.test.ts | 121 ++++ .../AppPermissionsDialogState.ts | 94 +++ .../getPermissionsDiff.test.ts | 83 +++ .../getPermissionsDiff.ts | 18 + .../components/AppPermissionsDialog/index.ts | 1 + .../AppPermissionsDialog/messages.ts | 78 +++ .../components/AppPermissionsDialog/types.ts | 6 + .../useGetAvailableAppPermissions.test.ts | 103 ++++ .../useGetAvailableAppPermissions.ts | 45 ++ .../InstallWithManifestFormButton.tsx | 8 +- src/apps/mutations.ts | 16 + src/graphql/hooks.generated.ts | 42 ++ src/graphql/types.generated.ts | 8 + 25 files changed, 1057 insertions(+), 565 deletions(-) create mode 100644 .changeset/rude-foxes-drop.md create mode 100644 .changeset/wicked-berries-watch.md create mode 100644 src/apps/components/AppPermissionsDialog/AppPermissionsDialog.tsx create mode 100644 src/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation.tsx create mode 100644 src/apps/components/AppPermissionsDialog/AppPermissionsDialogPermissionPicker.tsx create mode 100644 src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.test.ts create mode 100644 src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.ts create mode 100644 src/apps/components/AppPermissionsDialog/getPermissionsDiff.test.ts create mode 100644 src/apps/components/AppPermissionsDialog/getPermissionsDiff.ts create mode 100644 src/apps/components/AppPermissionsDialog/index.ts create mode 100644 src/apps/components/AppPermissionsDialog/messages.ts create mode 100644 src/apps/components/AppPermissionsDialog/types.ts create mode 100644 src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.test.ts create mode 100644 src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.ts diff --git a/.changeset/rude-foxes-drop.md b/.changeset/rude-foxes-drop.md new file mode 100644 index 000000000..2ceaa8904 --- /dev/null +++ b/.changeset/rude-foxes-drop.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Add possibility to manually edit permissions of the app. Now every user with MANAGE_APPS permission can grant any permission to the app via App -> Manage App view or remove permissions previously assigned. diff --git a/.changeset/wicked-berries-watch.md b/.changeset/wicked-berries-watch.md new file mode 100644 index 000000000..57f57f068 --- /dev/null +++ b/.changeset/wicked-berries-watch.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +App's "about" section will be rendered as a plain text, instead of a markdown diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 92a9d33ab..bf96b69de 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -770,6 +770,9 @@ "3PVGWj": { "string": "Filter preset" }, + "3SVI5p": { + "string": "Warning" + }, "3Sz1/t": { "context": "dialog header", "string": "Delete Pages" @@ -870,6 +873,9 @@ "context": "voucher discount", "string": "Specific products" }, + "47hJzu": { + "string": "Updated app permissions" + }, "483Xnh": { "context": "open full-screen", "string": "Open" @@ -1250,6 +1256,9 @@ "6udlH+": { "string": "Order draft successfully created" }, + "6uy2gU": { + "string": "Manually change permission for the app." + }, "6xC/Ls": { "context": "informations about product seo, header", "string": "SEO Information" @@ -3820,6 +3829,9 @@ "context": "description", "string": "No results found" }, + "PlAdWI": { + "string": "You are going to" + }, "Pnj+JH": { "context": "key-value field input", "string": "Value" @@ -4234,6 +4246,9 @@ "context": "export filtered items to csv file", "string": "Current search ({number})" }, + "SceSNp": { + "string": "Remove following permissions:" + }, "Sjd7wm": { "context": "product filter label", "string": "Product" @@ -5400,6 +5415,9 @@ "context": "error message", "string": "Email address is not set" }, + "abpvEI": { + "string": "Removing permissions may cause app to break." + }, "ac+Y98": { "context": "app settings error", "string": "Failed to fetch app settings" @@ -5441,6 +5459,9 @@ "context": "range input label", "string": "Postal codes (end)" }, + "azj0kR": { + "string": "Adding permission allows app to have more access to your data." + }, "b+jcaN": { "string": "There are still fulfillments created for this order. Cancel the fulfillments first before you cancel the order." }, @@ -5666,6 +5687,9 @@ "context": "column picker search no results message", "string": "No results found" }, + "cS1wAx": { + "string": "I know what I'm doing - confirm" + }, "cVjewM": { "context": "label for radio button", "string": "Product prices are entered with tax" @@ -6201,6 +6225,9 @@ "context": "voucher", "string": "Times used" }, + "hAoqp6": { + "string": "Failed to save permissions. Refresh the page and try again." + }, "hHOI7D": { "context": "product type name", "string": "Type Name" @@ -6535,6 +6562,9 @@ "jvKNMP": { "string": "Discount Code" }, + "jvo0vs": { + "string": "Save" + }, "jxoMLL": { "context": "product field", "string": "Collections" @@ -7190,6 +7220,9 @@ "oYGfnY": { "string": "ZIP / Postal code" }, + "oboeOT": { + "string": "Add following permissions" + }, "of/+iV": { "context": "transaction event type, refund was reversed, funds are back to store account", "string": "Refund reversed" @@ -7206,6 +7239,9 @@ "context": "unassign product from sale and save, button", "string": "Unassign and save" }, + "orvpWh": { + "string": "Go back" + }, "osPBn1": { "context": "currency filter label", "string": "Currency" @@ -7324,6 +7360,9 @@ "context": "checkbox label description", "string": "Expiration date will be automatically set, once gift card is issued" }, + "psmnv9": { + "string": "Edit permissions" + }, "ptPPVk": { "string": "No languages found" }, @@ -7550,6 +7589,9 @@ "context": "button", "string": "Delete variant" }, + "rbrahO": { + "string": "Close" + }, "reP5Uf": { "context": "global config plugin status popup description", "string": "Global plugins are set across all channels in your ecommerce. Only status is shown for those types of plugins" @@ -8421,6 +8463,9 @@ "context": "section header", "string": "Eligible Products" }, + "xrKHS6": { + "string": "Success" + }, "xrPv2K": { "context": "by preposition", "string": "by" diff --git a/package-lock.json b/package-lock.json index 2ba634c18..f081a1428 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,6 @@ "react-inlinesvg": "^3.0.2", "react-intl": "^5.21.2", "react-jss": "^10.0.0", - "react-markdown": "^4.3.1", "react-moment": "^1.0.0", "react-responsive-carousel": "^3.2.23", "react-router": "^5.0.1", @@ -14194,14 +14193,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bail": { - "version": "1.0.5", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "devOptional": true, @@ -15151,30 +15142,6 @@ "node": ">=10" } }, - "node_modules/character-entities": { - "version": "1.2.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "1.1.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "1.1.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/charcodes": { "version": "0.2.0", "dev": true, @@ -15624,14 +15591,6 @@ "version": "1.0.5", "license": "MIT" }, - "node_modules/collapse-white-space": { - "version": "1.0.6", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "license": "MIT", @@ -17763,6 +17722,7 @@ }, "node_modules/dom-serializer": { "version": "1.2.0", + "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", @@ -17775,6 +17735,7 @@ }, "node_modules/dom-serializer/node_modules/domhandler": { "version": "4.1.0", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" @@ -17788,6 +17749,7 @@ }, "node_modules/domelementtype": { "version": "2.2.0", + "dev": true, "funding": [ { "type": "github", @@ -17815,19 +17777,6 @@ "node": ">=8" } }, - "node_modules/domhandler": { - "version": "3.3.0", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.0.1" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, "node_modules/dompurify": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz", @@ -17835,6 +17784,7 @@ }, "node_modules/domutils": { "version": "2.8.0", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", @@ -17847,6 +17797,7 @@ }, "node_modules/domutils/node_modules/domhandler": { "version": "4.3.1", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" @@ -20816,7 +20767,8 @@ }, "node_modules/extend": { "version": "3.0.2", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/extend-shallow": { "version": "3.0.2", @@ -22582,32 +22534,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/html-to-react": { - "version": "1.4.5", - "license": "MIT", - "dependencies": { - "domhandler": "^3.3.0", - "htmlparser2": "^5.0", - "lodash.camelcase": "^4.3.0", - "ramda": "^0.27.1" - }, - "peerDependencies": { - "react": "^16.0 || ^17.0" - } - }, - "node_modules/htmlparser2": { - "version": "5.0.1", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^3.3.0", - "domutils": "^2.4.2", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/fb55/htmlparser2?sponsor=1" - } - }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -22924,6 +22850,7 @@ }, "node_modules/inherits": { "version": "2.0.4", + "devOptional": true, "license": "ISC" }, "node_modules/ini": { @@ -23140,26 +23067,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-alphabetical": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-arguments": { "version": "1.1.0", "license": "MIT", @@ -23202,6 +23109,7 @@ }, "node_modules/is-buffer": { "version": "1.1.6", + "devOptional": true, "license": "MIT" }, "node_modules/is-callable": { @@ -23270,14 +23178,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-decimal": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-deflate": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", @@ -23384,14 +23284,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-hexadecimal": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-in-browser": { "version": "1.1.3", "license": "MIT" @@ -23531,6 +23423,7 @@ }, "node_modules/is-plain-obj": { "version": "1.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -23744,14 +23637,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-whitespace-character": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-windows": { "version": "1.0.2", "dev": true, @@ -23760,14 +23645,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-word-character": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -26752,10 +26629,6 @@ "version": "4.17.21", "license": "MIT" }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "license": "MIT" - }, "node_modules/lodash.clone": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", @@ -27220,14 +27093,6 @@ "node": ">=0.10.0" } }, - "node_modules/markdown-escapes": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/markdown-it": { "version": "12.3.2", "license": "MIT", @@ -27285,13 +27150,6 @@ "is-buffer": "~1.1.6" } }, - "node_modules/mdast-add-list-metadata": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "unist-util-visit-parents": "1.1.2" - } - }, "node_modules/mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", @@ -28977,18 +28835,6 @@ "node": ">=6" } }, - "node_modules/parse-entities": { - "version": "1.2.2", - "license": "MIT", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, "node_modules/parse-filepath": { "version": "1.0.2", "dev": true, @@ -29840,10 +29686,6 @@ "performance-now": "^2.1.0" } }, - "node_modules/ramda": { - "version": "0.27.1", - "license": "MIT" - }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -30271,27 +30113,6 @@ "version": "3.0.4", "license": "MIT" }, - "node_modules/react-markdown": { - "version": "4.3.1", - "license": "MIT", - "dependencies": { - "html-to-react": "^1.3.4", - "mdast-add-list-metadata": "1.0.1", - "prop-types": "^15.7.2", - "react-is": "^16.8.6", - "remark-parse": "^5.0.0", - "unified": "^6.1.5", - "unist-util-visit": "^1.3.0", - "xtend": "^4.0.1" - }, - "peerDependencies": { - "react": "^15.0.0 || ^16.0.0" - } - }, - "node_modules/react-markdown/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, "node_modules/react-moment": { "version": "1.1.1", "license": "MIT", @@ -31141,27 +30962,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/remark-parse": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.1.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, "node_modules/remark-slug": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/remark-slug/-/remark-slug-6.1.0.tgz", @@ -31244,18 +31044,12 @@ }, "node_modules/repeat-string": { "version": "1.6.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10" } }, - "node_modules/replace-ext": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/replaceall": { "version": "0.1.6", "dev": true, @@ -32685,14 +32479,6 @@ "node": ">=8" } }, - "node_modules/state-toggle": { - "version": "1.0.3", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/static-extend": { "version": "0.1.2", "dev": true, @@ -33596,9 +33382,6 @@ "node": ">=0.6" } }, - "node_modules/trim": { - "version": "0.0.1" - }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -33608,22 +33391,6 @@ "node": ">=8" } }, - "node_modules/trim-trailing-lines": { - "version": "1.1.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "1.0.5", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -34237,18 +34004,6 @@ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", "optional": true }, - "node_modules/unherit": { - "version": "1.1.3", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -34304,18 +34059,6 @@ "node": ">=4" } }, - "node_modules/unified": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^2.0.0", - "x-is-string": "^0.1.0" - } - }, "node_modules/union-value": { "version": "1.0.1", "dev": true, @@ -34340,43 +34083,6 @@ "node": ">=8" } }, - "node_modules/unist-util-is": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/unist-util-remove-position": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "unist-util-visit": "^1.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/unist-util-visit": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/unist-util-visit/node_modules/unist-util-visit-parents": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "unist-util-is": "^3.0.0" - } - }, "node_modules/universalify": { "version": "0.1.2", "devOptional": true, @@ -34867,31 +34573,6 @@ "extsprintf": "^1.2.0" } }, - "node_modules/vfile": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.4", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" - } - }, - "node_modules/vfile-location": { - "version": "2.0.6", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "unist-util-stringify-position": "^1.1.1" - } - }, "node_modules/vite": { "version": "3.2.4", "dev": true, @@ -35916,9 +35597,6 @@ } } }, - "node_modules/x-is-string": { - "version": "0.1.0" - }, "node_modules/xdg-basedir": { "version": "4.0.0", "license": "MIT", @@ -35969,6 +35647,7 @@ "node_modules/xtend": { "version": "4.0.2", "license": "MIT", + "optional": true, "engines": { "node": ">=0.4" } @@ -46055,9 +45734,6 @@ "version": "1.0.2", "dev": true }, - "bail": { - "version": "1.0.5" - }, "balanced-match": { "version": "1.0.2", "devOptional": true @@ -46735,15 +46411,6 @@ "version": "1.0.2", "optional": true }, - "character-entities": { - "version": "1.2.4" - }, - "character-entities-legacy": { - "version": "1.1.4" - }, - "character-reference-invalid": { - "version": "1.1.4" - }, "charcodes": { "version": "0.2.0", "dev": true @@ -47032,9 +46699,6 @@ "codex-tooltip": { "version": "1.0.5" }, - "collapse-white-space": { - "version": "1.0.6" - }, "collect-v8-coverage": { "version": "1.0.1", "optional": true @@ -48517,6 +48181,7 @@ }, "dom-serializer": { "version": "1.2.0", + "dev": true, "requires": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -48525,6 +48190,7 @@ "dependencies": { "domhandler": { "version": "4.1.0", + "dev": true, "requires": { "domelementtype": "^2.2.0" } @@ -48532,7 +48198,8 @@ } }, "domelementtype": { - "version": "2.2.0" + "version": "2.2.0", + "dev": true }, "domexception": { "version": "2.0.1", @@ -48547,12 +48214,6 @@ } } }, - "domhandler": { - "version": "3.3.0", - "requires": { - "domelementtype": "^2.0.1" - } - }, "dompurify": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz", @@ -48560,6 +48221,7 @@ }, "domutils": { "version": "2.8.0", + "dev": true, "requires": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -48568,6 +48230,7 @@ "dependencies": { "domhandler": { "version": "4.3.1", + "dev": true, "requires": { "domelementtype": "^2.2.0" } @@ -50530,7 +50193,8 @@ } }, "extend": { - "version": "3.0.2" + "version": "3.0.2", + "optional": true }, "extend-shallow": { "version": "3.0.2", @@ -51753,24 +51417,6 @@ "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "optional": true }, - "html-to-react": { - "version": "1.4.5", - "requires": { - "domhandler": "^3.3.0", - "htmlparser2": "^5.0", - "lodash.camelcase": "^4.3.0", - "ramda": "^0.27.1" - } - }, - "htmlparser2": { - "version": "5.0.1", - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.3.0", - "domutils": "^2.4.2", - "entities": "^2.0.0" - } - }, "http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -51970,7 +51616,8 @@ } }, "inherits": { - "version": "2.0.4" + "version": "2.0.4", + "devOptional": true }, "ini": { "version": "1.3.8" @@ -52120,16 +51767,6 @@ } } }, - "is-alphabetical": { - "version": "1.0.4" - }, - "is-alphanumerical": { - "version": "1.0.4", - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, "is-arguments": { "version": "1.1.0", "optional": true, @@ -52152,7 +51789,8 @@ } }, "is-buffer": { - "version": "1.1.6" + "version": "1.1.6", + "devOptional": true }, "is-callable": { "version": "1.2.7", @@ -52193,9 +51831,6 @@ "version": "1.0.2", "devOptional": true }, - "is-decimal": { - "version": "1.0.4" - }, "is-deflate": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", @@ -52258,9 +51893,6 @@ "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", "optional": true }, - "is-hexadecimal": { - "version": "1.0.4" - }, "is-in-browser": { "version": "1.1.3" }, @@ -52335,7 +51967,8 @@ "version": "3.0.3" }, "is-plain-obj": { - "version": "1.1.0" + "version": "1.1.0", + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -52462,16 +52095,10 @@ "get-intrinsic": "^1.1.1" } }, - "is-whitespace-character": { - "version": "1.0.4" - }, "is-windows": { "version": "1.0.2", "dev": true }, - "is-word-character": { - "version": "1.0.4" - }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -54535,9 +54162,6 @@ "lodash-es": { "version": "4.17.21" }, - "lodash.camelcase": { - "version": "4.3.0" - }, "lodash.clone": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", @@ -54893,9 +54517,6 @@ "object-visit": "^1.0.0" } }, - "markdown-escapes": { - "version": "1.0.4" - }, "markdown-it": { "version": "12.3.2", "requires": { @@ -54932,12 +54553,6 @@ "is-buffer": "~1.1.6" } }, - "mdast-add-list-metadata": { - "version": "1.0.1", - "requires": { - "unist-util-visit-parents": "1.1.2" - } - }, "mdast-util-definitions": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", @@ -56087,17 +55702,6 @@ } } }, - "parse-entities": { - "version": "1.2.2", - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, "parse-filepath": { "version": "1.0.2", "dev": true, @@ -56712,9 +56316,6 @@ "performance-now": "^2.1.0" } }, - "ramda": { - "version": "0.27.1" - }, "randombytes": { "version": "2.1.0", "dev": true, @@ -57010,24 +56611,6 @@ "react-lifecycles-compat": { "version": "3.0.4" }, - "react-markdown": { - "version": "4.3.1", - "requires": { - "html-to-react": "^1.3.4", - "mdast-add-list-metadata": "1.0.1", - "prop-types": "^15.7.2", - "react-is": "^16.8.6", - "remark-parse": "^5.0.0", - "unified": "^6.1.5", - "unist-util-visit": "^1.3.0", - "xtend": "^4.0.1" - }, - "dependencies": { - "react-is": { - "version": "16.13.1" - } - } - }, "react-moment": { "version": "1.1.1" }, @@ -57635,26 +57218,6 @@ } } }, - "remark-parse": { - "version": "5.0.0", - "requires": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.1.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, "remark-slug": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/remark-slug/-/remark-slug-6.1.0.tgz", @@ -57712,10 +57275,8 @@ "dev": true }, "repeat-string": { - "version": "1.6.1" - }, - "replace-ext": { - "version": "1.0.0" + "version": "1.6.1", + "dev": true }, "replaceall": { "version": "0.1.6", @@ -58765,9 +58326,6 @@ } } }, - "state-toggle": { - "version": "1.0.3" - }, "static-extend": { "version": "0.1.2", "dev": true, @@ -59434,21 +58992,12 @@ "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", "optional": true }, - "trim": { - "version": "0.0.1" - }, "trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, - "trim-trailing-lines": { - "version": "1.1.4" - }, - "trough": { - "version": "1.0.5" - }, "ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -59872,13 +59421,6 @@ "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", "optional": true }, - "unherit": { - "version": "1.1.3", - "requires": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - } - }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -59924,17 +59466,6 @@ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "devOptional": true }, - "unified": { - "version": "6.2.0", - "requires": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^2.0.0", - "x-is-string": "^0.1.0" - } - }, "union-value": { "version": "1.0.1", "dev": true, @@ -59951,35 +59482,6 @@ "crypto-random-string": "^2.0.0" } }, - "unist-util-is": { - "version": "3.0.0" - }, - "unist-util-remove-position": { - "version": "1.1.4", - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "unist-util-stringify-position": { - "version": "1.1.2" - }, - "unist-util-visit": { - "version": "1.4.1", - "requires": { - "unist-util-visit-parents": "^2.0.0" - }, - "dependencies": { - "unist-util-visit-parents": { - "version": "2.1.2", - "requires": { - "unist-util-is": "^3.0.0" - } - } - } - }, - "unist-util-visit-parents": { - "version": "1.1.2" - }, "universalify": { "version": "0.1.2", "devOptional": true @@ -60291,24 +59793,6 @@ "extsprintf": "^1.2.0" } }, - "vfile": { - "version": "2.3.0", - "requires": { - "is-buffer": "^1.1.4", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" - } - }, - "vfile-location": { - "version": "2.0.6" - }, - "vfile-message": { - "version": "1.1.1", - "requires": { - "unist-util-stringify-position": "^1.1.1" - } - }, "vite": { "version": "3.2.4", "dev": true, @@ -61014,9 +60498,6 @@ "version": "7.5.9", "devOptional": true }, - "x-is-string": { - "version": "0.1.0" - }, "xdg-basedir": { "version": "4.0.0" }, @@ -61052,7 +60533,8 @@ "optional": true }, "xtend": { - "version": "4.0.2" + "version": "4.0.2", + "optional": true }, "y18n": { "version": "4.0.3", diff --git a/package.json b/package.json index c4c74a1dc..32ef106f8 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "react-inlinesvg": "^3.0.2", "react-intl": "^5.21.2", "react-jss": "^10.0.0", - "react-markdown": "^4.3.1", "react-moment": "^1.0.0", "react-responsive-carousel": "^3.2.23", "react-router": "^5.0.1", diff --git a/src/apps/components/AppDetailsPage/AboutCard.tsx b/src/apps/components/AppDetailsPage/AboutCard.tsx index 86b1537f0..f5e93944a 100644 --- a/src/apps/components/AppDetailsPage/AboutCard.tsx +++ b/src/apps/components/AppDetailsPage/AboutCard.tsx @@ -2,12 +2,11 @@ import Skeleton from "@dashboard/components/Skeleton"; import { Box, BoxProps, Text } from "@saleor/macaw-ui/next"; import React from "react"; import { useIntl } from "react-intl"; -import ReactMarkdown from "react-markdown"; import messages from "./messages"; type AboutCardProps = { - aboutApp?: string | null; + aboutApp: string | null; loading: boolean; } & BoxProps; @@ -24,7 +23,7 @@ export const AboutCard: React.FC = ({ } if (aboutApp) { - return ; + return {aboutApp}; } if (!aboutApp) { diff --git a/src/apps/components/AppDetailsPage/AppDetailsPage.tsx b/src/apps/components/AppDetailsPage/AppDetailsPage.tsx index 765a600d7..5b0fb32a8 100644 --- a/src/apps/components/AppDetailsPage/AppDetailsPage.tsx +++ b/src/apps/components/AppDetailsPage/AppDetailsPage.tsx @@ -45,6 +45,7 @@ export const AppDetailsPage: React.FC = ({ /> | null; loading: boolean; + appId: string; // todo wrap with App Context } & BoxProps; export const PermissionsCard: React.FC = ({ permissions, loading, + appId, ...boxProps }) => { + const [editPermissionDialogOpen, setEditPermissionDialogOpen] = + useState(false); const intl = useIntl(); + const editPermissionsButton = ( + + ); + const renderContent = () => { if (loading) { return ; } if (permissions && permissions.length === 0) { - return {intl.formatMessage(messages.appNoPermissions)}; + return ( + <> + + {intl.formatMessage(messages.appNoPermissions)} + + {editPermissionsButton} + + ); } if (permissions && permissions.length > 0) { @@ -43,6 +64,7 @@ export const PermissionsCard: React.FC = ({ ))} + {editPermissionsButton} ); } @@ -51,11 +73,20 @@ export const PermissionsCard: React.FC = ({ }; return ( - - - {intl.formatMessage(messages.appPermissionsTitle)} - - {renderContent()} - + <> + {editPermissionDialogOpen && ( + setEditPermissionDialogOpen(false)} + assignedPermissions={permissions?.map(p => p.code) ?? []} + /> + )} + + + {intl.formatMessage(messages.appPermissionsTitle)} + + {renderContent()} + + ); }; diff --git a/src/apps/components/AppDetailsPage/messages.ts b/src/apps/components/AppDetailsPage/messages.ts index 8a8c411b1..87bc4d884 100644 --- a/src/apps/components/AppDetailsPage/messages.ts +++ b/src/apps/components/AppDetailsPage/messages.ts @@ -43,4 +43,8 @@ export default defineMessages({ id: "b088Xv", defaultMessage: "App doesn't provide a description.", }, + editPermissionsButton: { + defaultMessage: "Edit permissions", + id: "psmnv9", + }, }); diff --git a/src/apps/components/AppPermissionsDialog/AppPermissionsDialog.tsx b/src/apps/components/AppPermissionsDialog/AppPermissionsDialog.tsx new file mode 100644 index 000000000..bc7d5b072 --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/AppPermissionsDialog.tsx @@ -0,0 +1,149 @@ +import { AppPermissionsDialogConfirmation } from "@dashboard/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation"; +import { AppPermissionsDialogPermissionPicker } from "@dashboard/apps/components/AppPermissionsDialog/AppPermissionsDialogPermissionPicker"; +import { useAppPermissionsDialogState } from "@dashboard/apps/components/AppPermissionsDialog/AppPermissionsDialogState"; +import { AppPermissionsDialogMessages } from "@dashboard/apps/components/AppPermissionsDialog/messages"; +import { useGetAvailableAppPermissions } from "@dashboard/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions"; +import { + PermissionEnum, + useAppQuery, + useAppUpdatePermissionsMutation, +} from "@dashboard/graphql"; +import useNotifier from "@dashboard/hooks/useNotifier"; +import { Dialog, DialogContent, DialogTitle } from "@material-ui/core"; +import { Skeleton } from "@material-ui/lab"; +import { Box, Text } from "@saleor/macaw-ui/next"; +import React, { useEffect } from "react"; +import { useIntl } from "react-intl"; + +const messages = AppPermissionsDialogMessages.dialogRoot; + +interface AppPermissionsDialogProps { + onClose: () => void; + assignedPermissions: PermissionEnum[]; + appId: string; +} + +export const AppPermissionsDialog = ({ + assignedPermissions, + onClose, + appId, +}: AppPermissionsDialogProps) => { + const { availablePermissions } = useGetAvailableAppPermissions(); + const { formatMessage } = useIntl(); + const { + updateSelected, + onConfirmSelection, + state, + onBackFromConfirmation, + selectedPermissions, + onMutationError, + onApprove, + } = useAppPermissionsDialogState(assignedPermissions); + + const { refetch } = useAppQuery({ variables: { id: appId }, skip: true }); + + const notify = useNotifier(); + + const [mutate] = useAppUpdatePermissionsMutation({ + onError(err) { + onMutationError(err.message); + }, + onCompleted(data) { + if (data.appUpdate?.errors.length) { + onMutationError( + data.appUpdate?.errors[0].message ?? + formatMessage(messages.fallbackErrorText), + ); + + return; + } + + refetch().then(onClose); + + notify({ + status: "success", + title: formatMessage(messages.successNotificationTitle), + autohide: 1000, + text: formatMessage(messages.successNotificationBody), + }); + }, + }); + + useEffect(() => { + if (state.type === "saving") { + mutate({ + variables: { + permissions: state.selected, + id: appId, + }, + }); + } + }, [state.type, appId]); + + const renderDialogContent = () => { + switch (state.type) { + case "pick-permissions": + return ( + + ); + case "confirm-permissions": + return ( + + ); + + case "saving": + return ; + case "error": + return ( + + + {state.error} + + + ); + } + }; + + return ( + + + {formatMessage(messages.heading)} + + + + {formatMessage(messages.info)} + + + {formatMessage(messages.warningHeading)} + + {formatMessage(messages.warningParagraph1)} + {formatMessage(messages.warningParagraph2)} + + {renderDialogContent()} + + + + ); +}; diff --git a/src/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation.tsx b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation.tsx new file mode 100644 index 000000000..d37f52050 --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation.tsx @@ -0,0 +1,77 @@ +import { AppPermissionsDialogMessages } from "@dashboard/apps/components/AppPermissionsDialog/messages"; +import { useGetAvailableAppPermissions } from "@dashboard/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions"; +import { PermissionEnum } from "@dashboard/graphql"; +import { Box, Button, Text } from "@saleor/macaw-ui/next"; +import React from "react"; +import { useIntl } from "react-intl"; + +const messages = AppPermissionsDialogMessages.confirmation; + +interface Props { + removedPermissions: PermissionEnum[]; + addedPermissions: PermissionEnum[]; + onBack(): void; + onApprove(): void; +} + +export const AppPermissionsDialogConfirmation = ({ + removedPermissions, + addedPermissions, + onBack, + onApprove, +}: Props) => { + const isPermissionsAdded = addedPermissions.length > 0; + const isPermissionsRemoved = removedPermissions.length > 0; + const intl = useIntl(); + + const { mapCodesToNames } = useGetAvailableAppPermissions(); + + return ( + + + {intl.formatMessage(messages.summaryText)} + + {isPermissionsRemoved && ( + + + {intl.formatMessage(messages.removePermissions)} + + {mapCodesToNames(removedPermissions).map(perm => ( + + {perm} + + ))} + + )} + {isPermissionsAdded && ( + + + {intl.formatMessage(messages.addPermissions)} + + {mapCodesToNames(addedPermissions).map(perm => ( + + {perm} + + ))} + + )} + + + + + + ); +}; diff --git a/src/apps/components/AppPermissionsDialog/AppPermissionsDialogPermissionPicker.tsx b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogPermissionPicker.tsx new file mode 100644 index 000000000..048608e85 --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogPermissionPicker.tsx @@ -0,0 +1,80 @@ +import { AppPermissionsDialogMessages } from "@dashboard/apps/components/AppPermissionsDialog/messages"; +import { AppPermission } from "@dashboard/apps/components/AppPermissionsDialog/types"; +import { PermissionEnum } from "@dashboard/graphql"; +import { Box, Button, Checkbox, List, Text } from "@saleor/macaw-ui/next"; +import React from "react"; +import { useIntl } from "react-intl"; + +const messages = AppPermissionsDialogMessages.permissionsPicker; + +interface AppPermissionsDialogPermissionPickerProps { + allPermissions: AppPermission[]; + selected: PermissionEnum[]; + onSubmit(): void; + onChange(codes: PermissionEnum[]): void; + onClose(): void; +} + +export const AppPermissionsDialogPermissionPicker = ({ + onSubmit, + onChange, + allPermissions, + selected, + onClose, +}: AppPermissionsDialogPermissionPickerProps) => { + const intl = useIntl(); + + return ( +
{ + e.preventDefault(); + onSubmit(); + }} + onChange={e => { + const formdata = new FormData(e.currentTarget); + + // @ts-expect-error - for some reason TS doesnt see keys, values, entries methods on formdata. TODO + const values = Array.from(formdata.keys()) as PermissionEnum[]; + + onChange(values); + }} + > + + {allPermissions.map(perm => { + const isAssigned = Boolean(selected.find(p => p === perm.code)); + + return ( + + + + {perm.name} + + + ); + })} + + + + + +
+ ); +}; diff --git a/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.test.ts b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.test.ts new file mode 100644 index 000000000..b3ebbb06f --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.test.ts @@ -0,0 +1,121 @@ +import { useAppPermissionsDialogState } from "@dashboard/apps/components/AppPermissionsDialog/AppPermissionsDialogState"; +import { PermissionEnum } from "@dashboard/graphql"; +import { renderHook } from "@testing-library/react-hooks"; + +describe("useAppPermissionsDialogState", () => { + it("Creates state with initial permissions - empty", () => { + const { + result: { current }, + } = renderHook(() => useAppPermissionsDialogState([])); + + expect(current.state.type).toEqual("pick-permissions"); + expect(current.state.selected).toEqual([]); + }); + + it("Creates state with initial permissions", () => { + const { + result: { current }, + } = renderHook(() => + useAppPermissionsDialogState([ + PermissionEnum.MANAGE_CHANNELS, + PermissionEnum.MANAGE_ORDERS, + ]), + ); + + expect(current.state.type).toEqual("pick-permissions"); + expect(current.state.selected).toEqual([ + "MANAGE_CHANNELS", + "MANAGE_ORDERS", + ]); + }); + + describe("Transitions to confirmation screen with proper diff", () => { + test("One added permission", async () => { + const { + result: { current }, + waitFor, + } = renderHook(() => + useAppPermissionsDialogState([ + PermissionEnum.MANAGE_CHANNELS, + PermissionEnum.MANAGE_ORDERS, + ]), + ); + + current.updateSelected([ + PermissionEnum.MANAGE_CHANNELS, + PermissionEnum.MANAGE_ORDERS, + PermissionEnum.HANDLE_CHECKOUTS, + ]); + + current.onConfirmSelection(); + + waitFor(() => { + expect(current.state.type).toEqual("confirm-permissions"); + + if (current.state.type === "confirm-permissions") { + expect(current.state.removedPermissions).toEqual([]); + expect(current.state.addedPermissions).toEqual(["HANDLE_CHECKOUTS"]); + } else { + throw new Error(); + } + }); + }); + + test("One removed permission", async () => { + const { + result: { current }, + waitFor, + } = renderHook(() => + useAppPermissionsDialogState([ + PermissionEnum.MANAGE_CHANNELS, + PermissionEnum.MANAGE_ORDERS, + ]), + ); + + current.updateSelected([PermissionEnum.MANAGE_CHANNELS]); + + current.onConfirmSelection(); + + waitFor(() => { + expect(current.state.type).toEqual("confirm-permissions"); + + if (current.state.type === "confirm-permissions") { + expect(current.state.removedPermissions).toEqual(["MANAGE_ORDERS"]); + expect(current.state.addedPermissions).toEqual([""]); + } else { + throw new Error(); + } + }); + }); + + test("One added and one removed permission", async () => { + const { + result: { current }, + waitFor, + } = renderHook(() => + useAppPermissionsDialogState([ + PermissionEnum.MANAGE_CHANNELS, + PermissionEnum.MANAGE_ORDERS, + ]), + ); + + current.updateSelected([ + PermissionEnum.MANAGE_CHANNELS, + PermissionEnum.MANAGE_CHECKOUTS, + ]); + + current.onConfirmSelection(); + + waitFor(() => { + expect(current.state.type).toEqual("confirm-permissions"); + + if (current.state.type === "confirm-permissions") { + expect(current.state.removedPermissions).toEqual(["MANAGE_ORDERS"]); + expect(current.state.addedPermissions).toEqual(["HANDLE_CHECKOUTS"]); + } else { + throw new Error(); + } + }); + }); + }); +}); diff --git a/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.ts b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.ts new file mode 100644 index 000000000..dcb8d643e --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.ts @@ -0,0 +1,94 @@ +import { getPermissionsDiff } from "@dashboard/apps/components/AppPermissionsDialog/getPermissionsDiff"; +import { PermissionEnum } from "@dashboard/graphql"; +import { useState } from "react"; + +type State = + | { + type: "pick-permissions"; + selected: PermissionEnum[]; + } + | { + type: "confirm-permissions"; + selected: PermissionEnum[]; + addedPermissions: PermissionEnum[]; + removedPermissions: PermissionEnum[]; + } + | { + selected: PermissionEnum[]; + type: "saving"; + } + | { + selected: PermissionEnum[]; + type: "error"; + error: string; + }; + +export const useAppPermissionsDialogState = ( + initialPermissions: PermissionEnum[], +) => { + const [state, setState] = useState({ + type: "pick-permissions", + selected: initialPermissions, + }); + + return { + state, + stateType: state.type, + selectedPermissions: state.selected, + updateSelected(newPermissions: PermissionEnum[]) { + if (state.type !== "pick-permissions") { + throw new Error("Invalid state"); + } + + setState({ + type: "pick-permissions", + selected: newPermissions, + }); + }, + onConfirmSelection() { + if (state.type !== "pick-permissions") { + throw new Error("Invalid state"); + } + + const diff = getPermissionsDiff(initialPermissions, state.selected); + + setState({ + type: "confirm-permissions", + selected: state.selected, + addedPermissions: diff.added, + removedPermissions: diff.removed, + }); + }, + onApprove() { + if (state.type !== "confirm-permissions") { + throw new Error("Invalid state"); + } + + setState({ + type: "saving", + selected: state.selected, + }); + }, + onBackFromConfirmation() { + if (state.type !== "confirm-permissions") { + throw new Error("Invalid state"); + } + + setState({ + type: "pick-permissions", + selected: state.selected, + }); + }, + onMutationError(message: string) { + if (state.type !== "saving") { + throw new Error("Invalid state"); + } + + setState({ + type: "error", + error: message, + selected: state.selected, + }); + }, + }; +}; diff --git a/src/apps/components/AppPermissionsDialog/getPermissionsDiff.test.ts b/src/apps/components/AppPermissionsDialog/getPermissionsDiff.test.ts new file mode 100644 index 000000000..f15c5425e --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/getPermissionsDiff.test.ts @@ -0,0 +1,83 @@ +import { getPermissionsDiff } from "@dashboard/apps/components/AppPermissionsDialog/getPermissionsDiff"; +import { PermissionEnum } from "@dashboard/graphql"; + +describe("getPermissionsDiff", () => { + describe("Correctly resolves added permissions", () => { + test("From empty to one new", () => { + const { added, removed } = getPermissionsDiff( + [], + [PermissionEnum.HANDLE_CHECKOUTS], + ); + + expect(added).toEqual([PermissionEnum.HANDLE_CHECKOUTS]); + expect(removed).toEqual([]); + }); + + test("From 0 to 3 new", () => { + const { added, removed } = getPermissionsDiff( + [], + [ + PermissionEnum.IMPERSONATE_USER, + PermissionEnum.HANDLE_PAYMENTS, + PermissionEnum.MANAGE_APPS, + ], + ); + + expect(added).toEqual([ + PermissionEnum.IMPERSONATE_USER, + PermissionEnum.HANDLE_PAYMENTS, + PermissionEnum.MANAGE_APPS, + ]); + expect(removed).toEqual([]); + }); + + test("From 1 to 2 new and 1 existing", () => { + const { added, removed } = getPermissionsDiff( + [PermissionEnum.HANDLE_CHECKOUTS], + [ + PermissionEnum.HANDLE_CHECKOUTS, + PermissionEnum.HANDLE_PAYMENTS, + PermissionEnum.MANAGE_APPS, + ], + ); + + expect(added).toEqual([ + PermissionEnum.HANDLE_PAYMENTS, + PermissionEnum.MANAGE_APPS, + ]); + expect(removed).toEqual([]); + }); + }); + + describe("Correctly resolves removed permissions", () => { + test("Doesnt remove anything if the same", () => { + const { added, removed } = getPermissionsDiff( + [PermissionEnum.HANDLE_CHECKOUTS], + [PermissionEnum.HANDLE_CHECKOUTS], + ); + + expect(added).toEqual([]); + expect(removed).toEqual([]); + }); + + test("Removes one", () => { + const { added, removed } = getPermissionsDiff( + [PermissionEnum.HANDLE_CHECKOUTS], + [], + ); + + expect(added).toEqual([]); + expect(removed).toEqual([PermissionEnum.HANDLE_CHECKOUTS]); + }); + }); + + test("Removes one and adds ", () => { + const { added, removed } = getPermissionsDiff( + [PermissionEnum.HANDLE_CHECKOUTS, PermissionEnum.HANDLE_PAYMENTS], + [PermissionEnum.HANDLE_CHECKOUTS, PermissionEnum.HANDLE_TAXES], + ); + + expect(added).toEqual([PermissionEnum.HANDLE_TAXES]); + expect(removed).toEqual([PermissionEnum.HANDLE_PAYMENTS]); + }); +}); diff --git a/src/apps/components/AppPermissionsDialog/getPermissionsDiff.ts b/src/apps/components/AppPermissionsDialog/getPermissionsDiff.ts new file mode 100644 index 000000000..ee50c8e48 --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/getPermissionsDiff.ts @@ -0,0 +1,18 @@ +import { PermissionEnum } from "@dashboard/graphql"; +import difference from "lodash/difference"; + +export const getPermissionsDiff = ( + initialPermissionsCodes: PermissionEnum[], + newPermissionsCodes: PermissionEnum[], +): { + added: PermissionEnum[]; + removed: PermissionEnum[]; +} => { + const removed = difference(initialPermissionsCodes, newPermissionsCodes); + const added = difference(newPermissionsCodes, initialPermissionsCodes); + + return { + added, + removed, + }; +}; diff --git a/src/apps/components/AppPermissionsDialog/index.ts b/src/apps/components/AppPermissionsDialog/index.ts new file mode 100644 index 000000000..9768a7433 --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/index.ts @@ -0,0 +1 @@ +export * from "./AppPermissionsDialog"; diff --git a/src/apps/components/AppPermissionsDialog/messages.ts b/src/apps/components/AppPermissionsDialog/messages.ts new file mode 100644 index 000000000..66d88232b --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/messages.ts @@ -0,0 +1,78 @@ +import { defineMessages } from "react-intl"; + +const confirmation = defineMessages({ + summaryText: { + defaultMessage: "You are going to", + id: "PlAdWI", + }, + addPermissions: { + defaultMessage: "Add following permissions", + id: "oboeOT", + }, + removePermissions: { + defaultMessage: "Remove following permissions:", + id: "SceSNp", + }, + backButton: { + defaultMessage: "Go back", + id: "orvpWh", + }, + confirmButton: { + defaultMessage: "I know what I'm doing - confirm", + id: "cS1wAx", + }, +}); + +const permissionsPicker = defineMessages({ + closeButton: { + defaultMessage: "Close", + id: "rbrahO", + }, + saveButton: { + defaultMessage: "Save", + id: "jvo0vs", + }, +}); + +const dialogRoot = defineMessages({ + heading: { + defaultMessage: "Edit permissions", + id: "psmnv9", + }, + info: { + defaultMessage: "Manually change permission for the app.", + id: "6uy2gU", + }, + warningHeading: { + defaultMessage: "Warning", + id: "3SVI5p", + }, + warningParagraph1: { + defaultMessage: + "Adding permission allows app to have more access to your data.", + id: "azj0kR", + }, + warningParagraph2: { + defaultMessage: "Removing permissions may cause app to break.", + id: "abpvEI", + }, + successNotificationTitle: { + defaultMessage: "Success", + id: "xrKHS6", + }, + successNotificationBody: { + defaultMessage: "Updated app permissions", + id: "47hJzu", + }, + fallbackErrorText: { + defaultMessage: + "Failed to save permissions. Refresh the page and try again.", + id: "hAoqp6", + }, +}); + +export const AppPermissionsDialogMessages = { + confirmation, + permissionsPicker, + dialogRoot, +}; diff --git a/src/apps/components/AppPermissionsDialog/types.ts b/src/apps/components/AppPermissionsDialog/types.ts new file mode 100644 index 000000000..b072ae23e --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/types.ts @@ -0,0 +1,6 @@ +import { PermissionEnum } from "@dashboard/graphql"; + +export interface AppPermission { + code: PermissionEnum; + name: string; +} diff --git a/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.test.ts b/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.test.ts new file mode 100644 index 000000000..8b5bc044a --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.test.ts @@ -0,0 +1,103 @@ +import { useGetAvailableAppPermissions } from "@dashboard/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions"; +import { PermissionEnum } from "@dashboard/graphql"; +import useShop from "@dashboard/hooks/useShop"; +import { renderHook } from "@testing-library/react-hooks"; + +type PermissionsFromApi = Array<{ + __typename: "Permission"; + code: PermissionEnum; + name: string; +}>; + +const getMockUseShopHookResult = () => { + const permissions: PermissionsFromApi = [ + { + __typename: "Permission", + name: "Manage Orders", + code: PermissionEnum.MANAGE_ORDERS, + }, + { + __typename: "Permission", + code: PermissionEnum.HANDLE_TAXES, + name: "Handle Taxes", + }, + { + __typename: "Permission", + code: PermissionEnum.MANAGE_CHANNELS, + name: "Manage Channels", + }, + { + __typename: "Permission", + code: PermissionEnum.MANAGE_APPS, + name: "Manage Apps", + }, + ]; + + return { + permissions, + }; +}; + +jest.mock("@dashboard/hooks/useShop"); + +describe("useGetAvailableAppPermissions", () => { + beforeEach(() => { + (useShop as jest.Mock).mockImplementationOnce(getMockUseShopHookResult); + }); + + it("Exposes permissons provided from useShop hook", () => { + const hookResult = renderHook(() => useGetAvailableAppPermissions()); + + expect(hookResult.result.current.availablePermissions).toEqual([ + { + name: "Manage Orders", + code: PermissionEnum.MANAGE_ORDERS, + }, + { + name: "Handle Taxes", + code: PermissionEnum.HANDLE_TAXES, + }, + { + code: PermissionEnum.MANAGE_CHANNELS, + name: "Manage Channels", + }, + ]); + }); + + it("Filters out MANAGE_APPS permission, because app should not have one", () => { + const hookResult = renderHook(() => useGetAvailableAppPermissions()); + + const resultPermissions = hookResult.result.current.availablePermissions; + const manageAppsPermission = resultPermissions.find( + perm => perm.code === PermissionEnum.MANAGE_APPS, + ); + + expect(manageAppsPermission).toBeUndefined(); + }); + + describe("mapCodesToNames method", () => { + it("Maps provided code enums and returns its names from the API", () => { + const hookResult = renderHook(() => useGetAvailableAppPermissions()); + + const resultNames = hookResult.result.current.mapCodesToNames([ + PermissionEnum.MANAGE_ORDERS, + PermissionEnum.HANDLE_TAXES, + ]); + + expect(resultNames).toEqual(["Manage Orders", "Handle Taxes"]); + }); + + it("Throws if useShop is not available", () => { + jest.resetAllMocks(); + (useShop as jest.Mock).mockImplementationOnce(() => undefined); + + const hookResult = renderHook(() => useGetAvailableAppPermissions()); + + expect(() => + hookResult.result.current.mapCodesToNames([ + PermissionEnum.MANAGE_ORDERS, + ]), + ).toThrow(); + }); + }); +}); diff --git a/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.ts b/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.ts new file mode 100644 index 000000000..1dc3080d4 --- /dev/null +++ b/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.ts @@ -0,0 +1,45 @@ +import { PermissionEnum } from "@dashboard/graphql"; +import useShop from "@dashboard/hooks/useShop"; + +export const useGetAvailableAppPermissions = () => { + const shopData = useShop(); + + /** + * App can't have MANAGE_APPS so filter it out + */ + const availablePermissions = shopData.permissions + .filter(perm => perm.code !== "MANAGE_APPS") + .map(p => ({ + code: p.code, + name: p.name, + })); + + const mapCodesToNames = (codes: PermissionEnum[]) => { + const permissions = shopData?.permissions; + + if (!permissions) { + throw new Error( + "Shop data from useShop hook is not available. mapCodesToNames method must be used after query resolves", + ); + } + + return codes.map(c => { + const relatedPermission = permissions.find(p => { + return p.code === c; + }); + + if (!relatedPermission) { + throw new Error( + "Trying to match permission enum from app that doesnt match available permissions from API", + ); + } + + return relatedPermission.name; + }); + }; + + return { + availablePermissions, + mapCodesToNames, + }; +}; diff --git a/src/apps/components/InstallWithManifestFormButton/InstallWithManifestFormButton.tsx b/src/apps/components/InstallWithManifestFormButton/InstallWithManifestFormButton.tsx index c64486598..b46184600 100644 --- a/src/apps/components/InstallWithManifestFormButton/InstallWithManifestFormButton.tsx +++ b/src/apps/components/InstallWithManifestFormButton/InstallWithManifestFormButton.tsx @@ -8,13 +8,13 @@ import { FormattedMessage, useIntl } from "react-intl"; import { messages } from "./messages"; import { useStyles } from "./styles"; -interface Props { +interface InstallWithManifestFormButtonProps { onSubmitted(manifestUrl: string): void; } -export const InstallWithManifestFormButton: React.FC = ({ - onSubmitted, -}) => { +export const InstallWithManifestFormButton: React.FC< + InstallWithManifestFormButtonProps +> = ({ onSubmitted }) => { const styles = useStyles(); const intl = useIntl(); diff --git a/src/apps/mutations.ts b/src/apps/mutations.ts index f3e5f8819..598774d89 100644 --- a/src/apps/mutations.ts +++ b/src/apps/mutations.ts @@ -157,3 +157,19 @@ export const appDeactivateMutation = gql` } } `; + +export const appUpdatePermissions = gql` + mutation AppUpdatePermissions($id: ID!, $permissions: [PermissionEnum!]!) { + appUpdate(id: $id, input: { permissions: $permissions }) { + app { + permissions { + code + name + } + } + errors { + message + } + } + } +`; diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 741adef37..e3520ea62 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -3585,6 +3585,48 @@ export function useAppDeactivateMutation(baseOptions?: ApolloReactHooks.Mutation export type AppDeactivateMutationHookResult = ReturnType; export type AppDeactivateMutationResult = Apollo.MutationResult; export type AppDeactivateMutationOptions = Apollo.BaseMutationOptions; +export const AppUpdatePermissionsDocument = gql` + mutation AppUpdatePermissions($id: ID!, $permissions: [PermissionEnum!]!) { + appUpdate(id: $id, input: {permissions: $permissions}) { + app { + permissions { + code + name + } + } + errors { + message + } + } +} + `; +export type AppUpdatePermissionsMutationFn = Apollo.MutationFunction; + +/** + * __useAppUpdatePermissionsMutation__ + * + * To run a mutation, you first call `useAppUpdatePermissionsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAppUpdatePermissionsMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [appUpdatePermissionsMutation, { data, loading, error }] = useAppUpdatePermissionsMutation({ + * variables: { + * id: // value for 'id' + * permissions: // value for 'permissions' + * }, + * }); + */ +export function useAppUpdatePermissionsMutation(baseOptions?: ApolloReactHooks.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useMutation(AppUpdatePermissionsDocument, options); + } +export type AppUpdatePermissionsMutationHookResult = ReturnType; +export type AppUpdatePermissionsMutationResult = Apollo.MutationResult; +export type AppUpdatePermissionsMutationOptions = Apollo.BaseMutationOptions; export const AppsListDocument = gql` query AppsList($before: String, $after: String, $first: Int, $last: Int, $sort: AppSortingInput, $filter: AppFilterInput) { apps( diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 423412ccf..824b0928c 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -7968,6 +7968,14 @@ export type AppDeactivateMutationVariables = Exact<{ export type AppDeactivateMutation = { __typename: 'Mutation', appDeactivate: { __typename: 'AppDeactivate', errors: Array<{ __typename: 'AppError', field: string | null, message: string | null, code: AppErrorCode, permissions: Array | null }> } | null }; +export type AppUpdatePermissionsMutationVariables = Exact<{ + id: Scalars['ID']; + permissions: Array | PermissionEnum; +}>; + + +export type AppUpdatePermissionsMutation = { __typename: 'Mutation', appUpdate: { __typename: 'AppUpdate', app: { __typename: 'App', permissions: Array<{ __typename: 'Permission', code: PermissionEnum, name: string }> | null } | null, errors: Array<{ __typename: 'AppError', message: string | null }> } | null }; + export type AppsListQueryVariables = Exact<{ before?: InputMaybe; after?: InputMaybe;