diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b89669cb..f06456061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable, unreleased changes to this project will be documented in this file. - Fix minor bugs - #244 by @dominik-zeglen - Fix tax settings updating - #243 by @dominik-zeglen +- Add secret fields in plugin configuration - #246 by @dominik-zeglen - Fix subcategories pagination - #249 by @dominik-zeglen ## 2.0.0 diff --git a/locale/messages.pot b/locale/messages.pot index d1446a81e..394dd6744 100644 --- a/locale/messages.pot +++ b/locale/messages.pot @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2019-11-07T13:41:50.654Z\n" +"POT-Creation-Date: 2019-11-08T10:47:25.155Z\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "MIME-Version: 1.0\n" @@ -223,6 +223,14 @@ msgctxt "add attribute value" msgid "Add Value" msgstr "" +#: build/locale/src/plugins/components/PluginSecretFieldDialog/PluginSecretFieldDialog.json +#. [src.plugins.components.PluginSecretFieldDialog.3032939398] - header +#. defaultMessage is: +#. Add Value to Authorization Field +msgctxt "header" +msgid "Add Value to Authorization Field" +msgstr "" + #: build/locale/src/shipping/components/ShippingZoneRateDialog/ShippingZoneRateDialog.json #. [src.shipping.components.ShippingZoneRateDialog.1397795758] - add weight based shipping method, dialog header #. defaultMessage is: @@ -1499,6 +1507,22 @@ msgctxt "authentication provider name" msgid "Authentication type" msgstr "" +#: build/locale/src/plugins/components/PluginAuthorization/PluginAuthorization.json +#. [src.plugins.components.PluginAuthorization.768575093] - section header +#. defaultMessage is: +#. Authorization +msgctxt "section header" +msgid "Authorization" +msgstr "" + +#: build/locale/src/plugins/views/PluginsDetails.json +#. [src.plugins.views.1096544448] - header +#. defaultMessage is: +#. Authorization Field Delete +msgctxt "header" +msgid "Authorization Field Delete" +msgstr "" + #: build/locale/src/collections/components/CollectionList/CollectionList.json #. [src.collections.components.CollectionList.3326160357] - collection availability #. defaultMessage is: @@ -1915,6 +1939,14 @@ msgctxt "description" msgid "City" msgstr "" +#: build/locale/src/intl.json +#. [src.clear] - button +#. defaultMessage is: +#. Clear +msgctxt "button" +msgid "Clear" +msgstr "" + #: build/locale/src/discounts/components/VoucherList/VoucherList.json #. [src.discounts.components.VoucherList.78726751] - voucher code #. defaultMessage is: @@ -2275,6 +2307,14 @@ msgctxt "description" msgid "Country area" msgstr "" +#: build/locale/src/intl.json +#. [src.create] - button +#. defaultMessage is: +#. Create +msgctxt "button" +msgid "Create" +msgstr "" + #: build/locale/src/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog.json #. [src.products.components.ProductVariantCreateDialog.4120989039] - create multiple variants, button #. defaultMessage is: @@ -3523,6 +3563,14 @@ msgctxt "dialog title" msgid "Edit Address" msgstr "" +#: build/locale/src/plugins/components/PluginSecretFieldDialog/PluginSecretFieldDialog.json +#. [src.plugins.components.PluginSecretFieldDialog.4269236107] - header +#. defaultMessage is: +#. Edit Authorization Field +msgctxt "header" +msgid "Edit Authorization Field" +msgstr "" + #: build/locale/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.json #. [src.orders.components.OrderAddressEditDialog.3982060155] - dialog header #. defaultMessage is: @@ -7923,14 +7971,6 @@ msgctxt "order subtotal price" msgid "Subtotal" msgstr "" -#: build/locale/src/plugins/views/PluginsDetails.json -#. [src.plugins.views.100549097] - plugin success message -#. defaultMessage is: -#. Succesfully updated plugin settings -msgctxt "plugin success message" -msgid "Succesfully updated plugin settings" -msgstr "" - #: build/locale/src/auth/components/ResetPasswordSuccessPage/ResetPasswordSuccessPage.json #. [src.auth.components.ResetPasswordSuccessPage.4066297200] #. defaultMessage is: @@ -8095,6 +8135,14 @@ msgctxt "description" msgid "The new dashboard and the GraphQL API are preview-quality software." msgstr "" +#: build/locale/src/plugins/views/PluginsDetails.json +#. [src.plugins.views.955370043] +#. defaultMessage is: +#. The plugin may stop working after this field is cleared. Are you sure you want to proceed? +msgctxt "description" +msgid "The plugin may stop working after this field is cleared. Are you sure you want to proceed?" +msgstr "" + #: build/locale/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.json #. [src.customers.components.CustomerCreateAddress.401345057] #. defaultMessage is: @@ -8128,13 +8176,9 @@ msgid "There is no address to show for this customer" msgstr "" #: build/locale/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.json -#. [src.plugins.components.PluginsDetailsPage.2277745418] +#. [src.plugins.components.PluginsDetailsPage.3799756739] #. defaultMessage is: -#. These are general information about your store. They define what is the URL of your store and what is shown in brow sers taskbar. -msgctxt "description" -msgid "These are general information about your store. They define what is the URL of your store and what is shown in brow sers taskbar." -msgstr "" - +#. These are general information about your store. They define what is the URL of your store and what is shown in browsers taskbar. #: build/locale/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.json #. [src.siteSettings.components.SiteSettingsPage.3799756739] #. defaultMessage is: diff --git a/schema.graphql b/schema.graphql index 4d12f342c..0cf0146af 100644 --- a/schema.graphql +++ b/schema.graphql @@ -960,7 +960,7 @@ type CollectionUpdatePrivateMeta { type ConfigurationItem { name: String! - value: String! + value: String type: ConfigurationTypeFieldEnum helpText: String label: String @@ -974,6 +974,8 @@ input ConfigurationItemInput { enum ConfigurationTypeFieldEnum { STRING BOOLEAN + SECRET + PASSWORD } enum CountryCode { diff --git a/src/components/ActionDialog/ActionDialog.tsx b/src/components/ActionDialog/ActionDialog.tsx index 5607851ba..737ce0723 100644 --- a/src/components/ActionDialog/ActionDialog.tsx +++ b/src/components/ActionDialog/ActionDialog.tsx @@ -9,6 +9,7 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { buttonMessages } from "@saleor/intl"; +import { DialogProps } from "@saleor/types"; import ConfirmButton, { ConfirmButtonTransitionState } from "../ConfirmButton/ConfirmButton"; @@ -26,15 +27,13 @@ const useStyles = makeStyles( { name: "ActionDialog" } ); -interface ActionDialogProps { +interface ActionDialogProps extends DialogProps { children?: React.ReactNode; confirmButtonLabel?: string; confirmButtonState: ConfirmButtonTransitionState; maxWidth?: "xs" | "sm" | "md" | "lg" | "xl" | false; - open: boolean; title: string; variant?: "default" | "delete"; - onClose?(); onConfirm(); } diff --git a/src/intl.ts b/src/intl.ts index 61345e3f6..fee264968 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -86,10 +86,18 @@ export const buttonMessages = defineMessages({ defaultMessage: "Cancel", description: "button" }, + clear: { + defaultMessage: "Clear", + description: "button" + }, confirm: { defaultMessage: "Confirm", description: "button" }, + create: { + defaultMessage: "Create", + description: "button" + }, delete: { defaultMessage: "Delete", description: "button" diff --git a/src/plugins/components/PluginAuthorization/PluginAuthorization.tsx b/src/plugins/components/PluginAuthorization/PluginAuthorization.tsx new file mode 100644 index 000000000..a590141c3 --- /dev/null +++ b/src/plugins/components/PluginAuthorization/PluginAuthorization.tsx @@ -0,0 +1,105 @@ +import Button from "@material-ui/core/Button"; +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import makeStyles from "@material-ui/core/styles/makeStyles"; +import Typography from "@material-ui/core/Typography"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import CardTitle from "@saleor/components/CardTitle"; +import Hr from "@saleor/components/Hr"; +import { buttonMessages } from "@saleor/intl"; +import { Plugin_plugin_configuration } from "@saleor/plugins/types/Plugin"; +import { isSecretField } from "@saleor/plugins/utils"; +import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes"; + +interface PluginAuthorizationProps { + fields: Plugin_plugin_configuration[]; + onClear: (field: string) => void; + onEdit: (field: string) => void; +} + +const useStyles = makeStyles(theme => ({ + button: { + marginLeft: theme.spacing() + }, + hr: { + margin: theme.spacing(2, 0) + }, + item: { + alignItems: "center", + display: "flex" + }, + spacer: { + flex: 1 + } +})); + +const PluginAuthorization: React.FC = props => { + const { fields, onClear, onEdit } = props; + + const classes = useStyles(props); + const intl = useIntl(); + + const secretFields = fields.filter(field => + isSecretField(fields, field.name) + ); + + return ( + + + + {secretFields.map((field, fieldIndex) => ( + +
+ {field.type === ConfigurationTypeFieldEnum.SECRET ? ( +
+ {field.label} + {field.value !== null && ( + **** {field.value} + )} +
+ ) : ( + {field.label} + )} +
+ {field.value === null ? ( + + ) : ( + <> + + + + )} +
+ {fieldIndex !== secretFields.length - 1 && ( +
+ )} + + ))} + + + ); +}; + +PluginAuthorization.displayName = "PluginAuthorization"; +export default PluginAuthorization; diff --git a/src/plugins/components/PluginAuthorization/index.ts b/src/plugins/components/PluginAuthorization/index.ts new file mode 100644 index 000000000..ec48cff4b --- /dev/null +++ b/src/plugins/components/PluginAuthorization/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PluginAuthorization"; +export * from "./PluginAuthorization"; diff --git a/src/plugins/components/PluginSecretFieldDialog/PluginSecretFieldDialog.stories.tsx b/src/plugins/components/PluginSecretFieldDialog/PluginSecretFieldDialog.stories.tsx new file mode 100644 index 000000000..16028559b --- /dev/null +++ b/src/plugins/components/PluginSecretFieldDialog/PluginSecretFieldDialog.stories.tsx @@ -0,0 +1,36 @@ +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes"; +import Decorator from "../../../storybook/Decorator"; +import PluginSecretFieldDialog, { + PluginSecretFieldDialogProps +} from "./PluginSecretFieldDialog"; + +const props: PluginSecretFieldDialogProps = { + confirmButtonState: "default", + field: { + __typename: "ConfigurationItem", + helpText: "", + label: "Generic Secret Field", + name: "secret", + type: ConfigurationTypeFieldEnum.SECRET, + value: "value" + }, + onClose: () => undefined, + onConfirm: () => undefined, + open: true +}; + +storiesOf("Views / Plugins / Edit secret field", module) + .addDecorator(Decorator) + .add("secret key", () => ) + .add("password", () => ( + + )); diff --git a/src/plugins/components/PluginSecretFieldDialog/PluginSecretFieldDialog.tsx b/src/plugins/components/PluginSecretFieldDialog/PluginSecretFieldDialog.tsx new file mode 100644 index 000000000..82bea2488 --- /dev/null +++ b/src/plugins/components/PluginSecretFieldDialog/PluginSecretFieldDialog.tsx @@ -0,0 +1,99 @@ +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import TextField from "@material-ui/core/TextField"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import ConfirmButton, { + ConfirmButtonTransitionState +} from "@saleor/components/ConfirmButton"; +import Form from "@saleor/components/Form"; +import Skeleton from "@saleor/components/Skeleton"; +import { buttonMessages } from "@saleor/intl"; +import { maybe } from "@saleor/misc"; +import { Plugin_plugin_configuration } from "@saleor/plugins/types/Plugin"; +import { DialogProps } from "@saleor/types"; +import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes"; + +export interface PluginSecretFieldDialogFormData { + value: string; +} +export interface PluginSecretFieldDialogProps extends DialogProps { + confirmButtonState: ConfirmButtonTransitionState; + field: Plugin_plugin_configuration; + onConfirm: (data: PluginSecretFieldDialogFormData) => void; +} + +const PluginSecretFieldDialog: React.FC = ({ + confirmButtonState, + field, + onClose, + onConfirm, + open +}) => { + const intl = useIntl(); + + const initialForm: PluginSecretFieldDialogFormData = { + value: "" + }; + + return ( + + + {field ? ( + field.value === null ? ( + intl.formatMessage({ + defaultMessage: "Add Value to Authorization Field", + description: "header" + }) + ) : ( + intl.formatMessage({ + defaultMessage: "Edit Authorization Field", + description: "header" + }) + ) + ) : ( + + )} + +
+ {({ change, data, submit }) => ( + <> + + field.type) === + ConfigurationTypeFieldEnum.PASSWORD && "password" + } + value={data.value || ""} + onChange={change} + /> + + + + + + + + + )} +
+
+ ); +}; + +PluginSecretFieldDialog.displayName = "PluginSecretFieldDialog"; +export default PluginSecretFieldDialog; diff --git a/src/plugins/components/PluginSecretFieldDialog/index.ts b/src/plugins/components/PluginSecretFieldDialog/index.ts new file mode 100644 index 000000000..bd4d72512 --- /dev/null +++ b/src/plugins/components/PluginSecretFieldDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./PluginSecretFieldDialog"; +export * from "./PluginSecretFieldDialog"; diff --git a/src/plugins/components/PluginSettings/PluginSettings.tsx b/src/plugins/components/PluginSettings/PluginSettings.tsx index aba2a19de..d01110b93 100644 --- a/src/plugins/components/PluginSettings/PluginSettings.tsx +++ b/src/plugins/components/PluginSettings/PluginSettings.tsx @@ -9,6 +9,7 @@ import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes"; import React from "react"; import { useIntl } from "react-intl"; +import { Plugin_plugin_configuration } from "@saleor/plugins/types/Plugin"; import { FormData } from "../PluginsDetailsPage"; interface PluginSettingsProps { @@ -16,19 +17,26 @@ interface PluginSettingsProps { errors: FormErrors<"name" | "configuration">; disabled: boolean; onChange: (event: React.ChangeEvent) => void; - fields: Array<{ - name: string; - type: ConfigurationTypeFieldEnum | null; - value: string; - helpText: string | null; - label: string | null; - }>; + fields: Plugin_plugin_configuration[]; } -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ + authItem: { + display: "flex" + }, + button: { + marginRight: theme.spacing() + }, item: { - paddingBottom: 10, - paddingTop: 10 + "&:not(:last-child)": { + marginBottom: theme.spacing(3) + } + }, + itemLabel: { + fontWeight: 500 + }, + spacer: { + flex: 1 } })); @@ -41,6 +49,7 @@ const PluginSettings: React.FC = ({ }) => { const classes = useStyles({}); const intl = useIntl(); + return ( = ({ })} /> - {data.configuration.map((configuration, index) => ( -
- {fields[index].type === ConfigurationTypeFieldEnum.STRING && ( - - )} - {fields[index].type === ConfigurationTypeFieldEnum.BOOLEAN && ( - - )} -
- ))} + {data.configuration.map(field => { + const fieldData = fields.find( + configField => configField.name === field.name + ); + + return ( +
+ {fieldData.type === ConfigurationTypeFieldEnum.BOOLEAN ? ( + + ) : ( + + )} +
+ ); + })}
); diff --git a/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx b/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx index cf656775d..7258b19e7 100644 --- a/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx +++ b/src/plugins/components/PluginsDetailsPage/PluginsDetailsPage.tsx @@ -1,3 +1,4 @@ +import makeStyles from "@material-ui/core/styles/makeStyles"; import Typography from "@material-ui/core/Typography"; import AppHeader from "@saleor/components/AppHeader"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; @@ -13,7 +14,12 @@ import { ConfigurationItemInput } from "@saleor/types/globalTypes"; import React from "react"; import { useIntl } from "react-intl"; +import CardSpacer from "@saleor/components/CardSpacer"; +import Hr from "@saleor/components/Hr"; +import { ChangeEvent } from "@saleor/hooks/useForm"; +import { isSecretField } from "@saleor/plugins/utils"; import { Plugin_plugin } from "../../types/Plugin"; +import PluginAuthorization from "../PluginAuthorization"; import PluginInfo from "../PluginInfo"; import PluginSettings from "../PluginSettings"; @@ -28,27 +34,52 @@ export interface PluginsDetailsPageProps { plugin: Plugin_plugin; saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; + onClear: (field: string) => void; + onEdit: (field: string) => void; onSubmit: (data: FormData) => void; } -const PluginsDetailsPage: React.FC = ({ - disabled, - errors, - plugin, - saveButtonBarState, - onBack, - onSubmit -}) => { +const useStyles = makeStyles( + { + spacer: { + gridColumnEnd: "span 2" + } + }, + { + name: "PluginsDetailsPage" + } +); + +const PluginsDetailsPage: React.FC = props => { + const { + disabled, + errors, + plugin, + saveButtonBarState, + onBack, + onClear, + onEdit, + onSubmit + } = props; + + const classes = useStyles(props); const intl = useIntl(); const initialForm: FormData = { active: maybe(() => plugin.active, false), - configuration: maybe(() => plugin.configuration, []) + configuration: maybe(() => + plugin.configuration + .filter(field => !isSecretField(plugin.configuration, field.name)) + .map(field => ({ + ...field, + value: field.value || "" + })) + ) }; return (
{({ data, errors, hasChanged, submit, set, triggerChange }) => { - const onChange = event => { + const onChange = (event: ChangeEvent) => { const newData = { active: data.active, configuration: data.configuration @@ -94,7 +125,7 @@ const PluginsDetailsPage: React.FC = ({ {intl.formatMessage({ defaultMessage: - "These are general information about your store. They define what is the URL of your store and what is shown in brow sers taskbar." + "These are general information about your store. They define what is the URL of your store and what is shown in browsers taskbar." })}
@@ -106,6 +137,7 @@ const PluginsDetailsPage: React.FC = ({ /> {data.configuration && ( <> +
{intl.formatMessage({ @@ -120,13 +152,29 @@ const PluginsDetailsPage: React.FC = ({ })}
- plugin.configuration, [])} - errors={errors} - disabled={disabled} - onChange={onChange} - /> +
+ plugin.configuration, [])} + errors={errors} + disabled={disabled} + onChange={onChange} + /> + {maybe(() => + plugin.configuration.some(field => + isSecretField(plugin.configuration, field.name) + ) + ) && ( + <> + + + + )} +
)} diff --git a/src/plugins/fixtures.ts b/src/plugins/fixtures.ts index 4da3ce27b..17fbf19ba 100644 --- a/src/plugins/fixtures.ts +++ b/src/plugins/fixtures.ts @@ -30,7 +30,7 @@ export const plugin: Plugin_plugin = { label: "Username or account", name: "Username or account", type: ConfigurationTypeFieldEnum.STRING, - value: "" + value: "avatax_user" }, { __typename: "ConfigurationItem", @@ -38,8 +38,32 @@ export const plugin: Plugin_plugin = { label: "Password or license", name: "Password or license", type: ConfigurationTypeFieldEnum.STRING, + value: "TEM8S2-2ET83-CGKP1-DPSI2-EPZO1" + }, + { + __typename: "ConfigurationItem", + helpText: "This key will enable you to connect to Avatax API", + label: "API key", + name: "apiKey", + type: ConfigurationTypeFieldEnum.SECRET, + value: "9ab9" + }, + { + __typename: "ConfigurationItem", + helpText: "", + label: "Password", + name: "password", + type: ConfigurationTypeFieldEnum.PASSWORD, value: "" }, + { + __typename: "ConfigurationItem", + helpText: "", + label: "Empty Password", + name: "password-not-set", + type: ConfigurationTypeFieldEnum.PASSWORD, + value: null + }, { __typename: "ConfigurationItem", helpText: "Determines if Saleor should use Avatax sandbox API.", @@ -51,7 +75,6 @@ export const plugin: Plugin_plugin = { ], description: "Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna.", - id: "UGx1Z2luQ29uZmlndXJhdGlvbjoy", name: "Username or account" }; diff --git a/src/plugins/index.tsx b/src/plugins/index.tsx index 388c408da..53080b861 100644 --- a/src/plugins/index.tsx +++ b/src/plugins/index.tsx @@ -8,7 +8,8 @@ import { WindowTitle } from "../components/WindowTitle"; import { pluginsListPath, PluginsListUrlQueryParams, - pluginsPath + pluginsPath, + PluginsUrlQueryParams } from "./urls"; import PluginsDetailsComponent from "./views/PluginsDetails"; import PluginsListComponent from "./views/PluginsList"; @@ -21,7 +22,7 @@ const PluginList: React.FC> = ({ location }) => { const PageDetails: React.FC> = ({ match }) => { const qs = parseQs(location.search.substr(1)); - const params: PluginsListUrlQueryParams = qs; + const params: PluginsUrlQueryParams = qs; return ( pluginsListPath + "?" + stringifyQs(params); export const pluginsPath = (id: string) => urlJoin(pluginsSection, id); -export type PluginsUrlQueryParams = SingleAction; +export type PluginUrlDialog = "clear" | "edit"; +export type PluginsUrlQueryParams = Dialog & { + field?: string; +}; export const pluginsUrl = (id: string, params?: PluginsUrlQueryParams) => pluginsPath(encodeURIComponent(id)) + "?" + stringifyQs(params); diff --git a/src/plugins/utils.ts b/src/plugins/utils.ts new file mode 100644 index 000000000..5d10d2115 --- /dev/null +++ b/src/plugins/utils.ts @@ -0,0 +1,12 @@ +import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes"; +import { Plugin_plugin_configuration } from "./types/Plugin"; + +export function isSecretField( + config: Plugin_plugin_configuration[], + field: string +) { + return [ + ConfigurationTypeFieldEnum.PASSWORD, + ConfigurationTypeFieldEnum.SECRET + ].includes(config.find(configField => configField.name === field).type); +} diff --git a/src/plugins/views/PluginDetails.test.ts b/src/plugins/views/PluginDetails.test.ts new file mode 100644 index 000000000..af11fd2fe --- /dev/null +++ b/src/plugins/views/PluginDetails.test.ts @@ -0,0 +1,87 @@ +import { + ConfigurationItemInput, + ConfigurationTypeFieldEnum +} from "../../types/globalTypes"; +import { Plugin_plugin_configuration } from "../types/Plugin"; +import { getConfigurationInput } from "./PluginsDetails"; + +const baseConfig: Omit< + Plugin_plugin_configuration, + "name" | "type" | "value" +> = { + __typename: "ConfigurationItem", + helpText: "", + label: "" +}; + +const config: Plugin_plugin_configuration[] = [ + { + ...baseConfig, + name: "field-1", + type: ConfigurationTypeFieldEnum.STRING, + value: "val1" + }, + { + ...baseConfig, + name: "field-2", + type: ConfigurationTypeFieldEnum.STRING, + value: "val2" + }, + { + ...baseConfig, + name: "field-3", + type: ConfigurationTypeFieldEnum.PASSWORD, + value: "" + }, + { + ...baseConfig, + name: "field-4", + type: ConfigurationTypeFieldEnum.SECRET, + value: "val4" + } +]; + +const input: ConfigurationItemInput[] = [ + { + name: "field-1", + value: "value1" + }, + { + name: "field-2", + value: "value2" + }, + { + name: "field-3", + value: "value3" + }, + { + name: "field-4", + value: "value4" + } +]; + +test("Ensure that no secret is sent in input", () => { + const output: ConfigurationItemInput[] = getConfigurationInput(config, input); + + expect(output).toHaveLength(2); + expect( + output.find( + field => + config.find(configField => configField.name === field.name).type === + ConfigurationTypeFieldEnum.PASSWORD + ) + ).toBeFalsy(); + expect( + output.find( + field => + config.find(configField => configField.name === field.name).type === + ConfigurationTypeFieldEnum.SECRET + ) + ).toBeFalsy(); +}); + +test("Handles null input", () => { + const output = getConfigurationInput(null, null); + + expect(output).toBeNull(); +}); diff --git a/src/plugins/views/PluginsDetails.tsx b/src/plugins/views/PluginsDetails.tsx index 5e890dd0c..edd4f1b1f 100644 --- a/src/plugins/views/PluginsDetails.tsx +++ b/src/plugins/views/PluginsDetails.tsx @@ -1,30 +1,90 @@ +import DialogContentText from "@material-ui/core/DialogContentText"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import ActionDialog from "@saleor/components/ActionDialog"; import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; -import React from "react"; -import { useIntl } from "react-intl"; - +import { commonMessages } from "@saleor/intl"; +import { ConfigurationItemInput } from "@saleor/types/globalTypes"; import { getMutationState, maybe } from "../../misc"; import PluginsDetailsPage from "../components/PluginsDetailsPage"; +import PluginSecretFieldDialog from "../components/PluginSecretFieldDialog"; import { TypedPluginUpdate } from "../mutations"; import { TypedPluginsDetailsQuery } from "../queries"; -import { pluginsListUrl, PluginsListUrlQueryParams } from "../urls"; +import { Plugin_plugin_configuration } from "../types/Plugin"; +import { PluginUpdate } from "../types/PluginUpdate"; +import { + pluginsListUrl, + pluginsUrl, + PluginsUrlQueryParams, + PluginUrlDialog +} from "../urls"; +import { isSecretField } from "../utils"; export interface PluginsDetailsProps { id: string; - params: PluginsListUrlQueryParams; + params: PluginsUrlQueryParams; } -export const PluginsDetails: React.FC = ({ id }) => { +export function getConfigurationInput( + config: Plugin_plugin_configuration[] | null, + input: ConfigurationItemInput[] | null +): ConfigurationItemInput[] | null { + if (config === null || input === null) { + return null; + } + + return input + .filter(field => !isSecretField(config, field.name)) + .map(field => ({ + name: field.name, + value: field.value.toString() + })); +} + +export const PluginsDetails: React.FC = ({ + id, + params +}) => { const navigate = useNavigator(); const notify = useNotifier(); const intl = useIntl(); + const closeModal = () => + navigate( + pluginsUrl(id, { + ...params, + action: undefined, + field: undefined + }), + true + ); + + const openModal = (action: PluginUrlDialog, field?: string) => + navigate( + pluginsUrl(id, { + ...params, + action, + field + }) + ); + + const handleUpdate = (data: PluginUpdate) => { + if (data.pluginUpdate.errors.length === 0) { + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + closeModal(); + } + }; + return ( - - {(pluginUpdate, pluginUpdateOpts) => ( - - {PluginDetails => { + + {pluginDetails => ( + + {(pluginUpdate, pluginUpdateOpts) => { const formTransitionState = getMutationState( pluginUpdateOpts.called, pluginUpdateOpts.loading, @@ -36,62 +96,90 @@ export const PluginsDetails: React.FC = ({ id }) => { [] ); - if (formErrors.length) { - formErrors.map(error => { - notify({ - text: error.message - }); + const handleFieldUpdate = (value: string) => + pluginUpdate({ + variables: { + id, + input: { + configuration: [ + { + name: params.field, + value + } + ] + } + } }); - } else { - if (pluginUpdateOpts.data) { - notify({ - text: intl.formatMessage({ - defaultMessage: "Succesfully updated plugin settings", - description: "plugin success message" - }) - }); - } - } return ( <> PluginDetails.data.plugin.name)} + title={maybe(() => pluginDetails.data.plugin.name)} /> PluginDetails.data.plugin)} + saveButtonBarState={ + !params.action ? formTransitionState : "default" + } + plugin={maybe(() => pluginDetails.data.plugin)} onBack={() => navigate(pluginsListUrl())} - onSubmit={formData => { - const configurationInput = - formData.configuration && - formData.configuration.map(item => { - return { - name: item.name, - value: item.value.toString() - }; - }); + onClear={field => openModal("clear", field)} + onEdit={field => openModal("edit", field)} + onSubmit={formData => pluginUpdate({ variables: { id, input: { active: formData.active, - configuration: configurationInput - ? configurationInput - : null + configuration: getConfigurationInput( + pluginDetails.data.plugin.configuration, + formData.configuration + ) } } - }); - }} + }) + } /> + {maybe(() => pluginDetails.data.plugin.configuration) && ( + <> + handleFieldUpdate(null)} + > + + + + + + pluginDetails.data.plugin.configuration.find( + field => field.name === params.field + ) + )} + onClose={closeModal} + onConfirm={formData => handleFieldUpdate(formData.value)} + open={params.action === "edit" && !!params.field} + /> + + )} ); }} - + )} - + ); }; PluginsDetails.displayName = "PluginsDetails"; diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index ef3360572..9b17eea50 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -78529,6 +78529,18 @@ exports[`Storyshots Views / Pages / Page list no data 1`] = ` `; +exports[`Storyshots Views / Plugins / Edit secret field password 1`] = ` +
+`; + +exports[`Storyshots Views / Plugins / Edit secret field secret key 1`] = ` +
+`; + exports[`Storyshots Views / Plugins / Plugin details default 1`] = `
- These are general information about your store. They define what is the URL of your store and what is shown in brow sers taskbar. + These are general information about your store. They define what is the URL of your store and what is shown in browsers taskbar.
+
-
+
- - Plugin Settings -
+ + Plugin Settings + +
+
+
+
+
+
+
+ +
+ + +
+

+ Provide user or account details +

+
+
+
+
+ +
+ + +
+

+ Provide password or license details +

+
+
+
+ +
+
-
-
- -
- - -
-

- Provide user or account details -

-
+ Authorization + +
+
+
- -
- - + API key +
+
+ **** 9ab9 +
-

- Provide password or license details -

-
-
-
-
@@ -78839,7 +78989,7 @@ exports[`Storyshots Views / Plugins / Plugin details form errors 1`] = `
- These are general information about your store. They define what is the URL of your store and what is shown in brow sers taskbar. + These are general information about your store. They define what is the URL of your store and what is shown in browsers taskbar.
+
-
+
- - Plugin Settings -
+ + Plugin Settings + +
+
+
+
+
+
+
+ +
+ + +
+

+ Provide user or account details +

+
+
+
+
+ +
+ + +
+

+ Provide password or license details +

+
+
+
+ +
+
-
-
- -
- - -
-

- Provide user or account details -

-
+ Authorization + +
+
+
- -
- - + API key +
+
+ **** 9ab9 +
-

- Provide password or license details -

-
-
-
-
@@ -79113,7 +79401,7 @@ exports[`Storyshots Views / Plugins / Plugin details loading 1`] = `
- These are general information about your store. They define what is the URL of your store and what is shown in brow sers taskbar. + These are general information about your store. They define what is the URL of your store and what is shown in browsers taskbar.
+
+
+ +
+`; + +exports[`Storyshots Views / Plugins / Plugin details not configurable 1`] = ` +
+
+
+
+
+ Username or account Details +
+
+
+
+
+
- Plugin Settings + Plugin Information and Status
- This adress will be used to generate invoices and calculate shipping rates. Email adress you provide here will be used as a contact adress for your customers. + These are general information about your store. They define what is the URL of your store and what is shown in browsers taskbar.
- Plugin Settings + Plugin Information and Status
+ > +
+ Plugin Name +
+
+ Username or account +
+
+ Description +
+
+ Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna. +
+
+
+
+ Status +
+ +
diff --git a/src/storybook/stories/plugins/PluginDetailsPage.tsx b/src/storybook/stories/plugins/PluginDetailsPage.tsx index 18aced41b..61bb17e7a 100644 --- a/src/storybook/stories/plugins/PluginDetailsPage.tsx +++ b/src/storybook/stories/plugins/PluginDetailsPage.tsx @@ -13,6 +13,8 @@ const props: PluginsDetailsPageProps = { disabled: false, errors: [], onBack: () => undefined, + onClear: () => undefined, + onEdit: () => undefined, onSubmit: () => undefined, plugin, saveButtonBarState: "default" @@ -33,4 +35,13 @@ storiesOf("Views / Plugins / Plugin details", module) "Password or license" ] as Array).map(formError)} /> + )) + .add("not configurable", () => ( + )); diff --git a/src/types.ts b/src/types.ts index d1f0fe3a7..5bae68335 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,11 @@ export interface UserError { message: string; } +export interface DialogProps { + open: boolean; + onClose: () => void; +} + export interface ListSettings { columns?: TColumn[]; rowNumber: number; diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index bf3380c70..10b35084d 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -8,39 +8,41 @@ export enum AddressTypeEnum { BILLING = "BILLING", - SHIPPING = "SHIPPING", + SHIPPING = "SHIPPING" } export enum AttributeInputTypeEnum { DROPDOWN = "DROPDOWN", - MULTISELECT = "MULTISELECT", + MULTISELECT = "MULTISELECT" } export enum AttributeTypeEnum { PRODUCT = "PRODUCT", - VARIANT = "VARIANT", + VARIANT = "VARIANT" } export enum AttributeValueType { COLOR = "COLOR", GRADIENT = "GRADIENT", STRING = "STRING", - URL = "URL", + URL = "URL" } export enum AuthorizationKeyType { FACEBOOK = "FACEBOOK", - GOOGLE_OAUTH2 = "GOOGLE_OAUTH2", + GOOGLE_OAUTH2 = "GOOGLE_OAUTH2" } export enum CollectionPublished { HIDDEN = "HIDDEN", - PUBLISHED = "PUBLISHED", + PUBLISHED = "PUBLISHED" } export enum ConfigurationTypeFieldEnum { BOOLEAN = "BOOLEAN", - STRING = "STRING", + PASSWORD = "PASSWORD", + SECRET = "SECRET", + STRING = "STRING" } export enum CountryCode { @@ -293,23 +295,23 @@ export enum CountryCode { YT = "YT", ZA = "ZA", ZM = "ZM", - ZW = "ZW", + ZW = "ZW" } export enum DiscountStatusEnum { ACTIVE = "ACTIVE", EXPIRED = "EXPIRED", - SCHEDULED = "SCHEDULED", + SCHEDULED = "SCHEDULED" } export enum DiscountValueTypeEnum { FIXED = "FIXED", - PERCENTAGE = "PERCENTAGE", + PERCENTAGE = "PERCENTAGE" } export enum FulfillmentStatus { CANCELED = "CANCELED", - FULFILLED = "FULFILLED", + FULFILLED = "FULFILLED" } export enum LanguageCodeEnum { @@ -355,19 +357,19 @@ export enum LanguageCodeEnum { UK = "UK", VI = "VI", ZH_HANS = "ZH_HANS", - ZH_HANT = "ZH_HANT", + ZH_HANT = "ZH_HANT" } export enum OrderAction { CAPTURE = "CAPTURE", MARK_AS_PAID = "MARK_AS_PAID", REFUND = "REFUND", - VOID = "VOID", + VOID = "VOID" } export enum OrderDirection { ASC = "ASC", - DESC = "DESC", + DESC = "DESC" } export enum OrderEventsEmailsEnum { @@ -376,7 +378,7 @@ export enum OrderEventsEmailsEnum { ORDER_CONFIRMATION = "ORDER_CONFIRMATION", PAYMENT_CONFIRMATION = "PAYMENT_CONFIRMATION", SHIPPING_CONFIRMATION = "SHIPPING_CONFIRMATION", - TRACKING_UPDATED = "TRACKING_UPDATED", + TRACKING_UPDATED = "TRACKING_UPDATED" } export enum OrderEventsEnum { @@ -400,7 +402,7 @@ export enum OrderEventsEnum { PLACED = "PLACED", PLACED_FROM_DRAFT = "PLACED_FROM_DRAFT", TRACKING_UPDATED = "TRACKING_UPDATED", - UPDATED_ADDRESS = "UPDATED_ADDRESS", + UPDATED_ADDRESS = "UPDATED_ADDRESS" } export enum OrderStatus { @@ -408,7 +410,7 @@ export enum OrderStatus { DRAFT = "DRAFT", FULFILLED = "FULFILLED", PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED", - UNFULFILLED = "UNFULFILLED", + UNFULFILLED = "UNFULFILLED" } export enum OrderStatusFilter { @@ -417,7 +419,7 @@ export enum OrderStatusFilter { PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED", READY_TO_CAPTURE = "READY_TO_CAPTURE", READY_TO_FULFILL = "READY_TO_FULFILL", - UNFULFILLED = "UNFULFILLED", + UNFULFILLED = "UNFULFILLED" } export enum PaymentChargeStatusEnum { @@ -425,7 +427,7 @@ export enum PaymentChargeStatusEnum { FULLY_REFUNDED = "FULLY_REFUNDED", NOT_CHARGED = "NOT_CHARGED", PARTIALLY_CHARGED = "PARTIALLY_CHARGED", - PARTIALLY_REFUNDED = "PARTIALLY_REFUNDED", + PARTIALLY_REFUNDED = "PARTIALLY_REFUNDED" } export enum PermissionEnum { @@ -443,7 +445,7 @@ export enum PermissionEnum { MANAGE_STAFF = "MANAGE_STAFF", MANAGE_TRANSLATIONS = "MANAGE_TRANSLATIONS", MANAGE_USERS = "MANAGE_USERS", - MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS", + MANAGE_WEBHOOKS = "MANAGE_WEBHOOKS" } export enum ProductErrorCode { @@ -457,7 +459,7 @@ export enum ProductErrorCode { NOT_PRODUCTS_IMAGE = "NOT_PRODUCTS_IMAGE", REQUIRED = "REQUIRED", UNIQUE = "UNIQUE", - VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT", + VARIANT_NO_DIGITAL_CONTENT = "VARIANT_NO_DIGITAL_CONTENT" } export enum ProductOrderField { @@ -466,37 +468,37 @@ export enum ProductOrderField { NAME = "NAME", PRICE = "PRICE", PUBLISHED = "PUBLISHED", - TYPE = "TYPE", + TYPE = "TYPE" } export enum ProductTypeConfigurable { CONFIGURABLE = "CONFIGURABLE", - SIMPLE = "SIMPLE", + SIMPLE = "SIMPLE" } export enum ProductTypeEnum { DIGITAL = "DIGITAL", - SHIPPABLE = "SHIPPABLE", + SHIPPABLE = "SHIPPABLE" } export enum SaleType { FIXED = "FIXED", - PERCENTAGE = "PERCENTAGE", + PERCENTAGE = "PERCENTAGE" } export enum ShippingMethodTypeEnum { PRICE = "PRICE", - WEIGHT = "WEIGHT", + WEIGHT = "WEIGHT" } export enum StaffMemberStatus { ACTIVE = "ACTIVE", - DEACTIVATED = "DEACTIVATED", + DEACTIVATED = "DEACTIVATED" } export enum StockAvailability { IN_STOCK = "IN_STOCK", - OUT_OF_STOCK = "OUT_OF_STOCK", + OUT_OF_STOCK = "OUT_OF_STOCK" } export enum TaxRateType { @@ -524,19 +526,19 @@ export enum TaxRateType { SOCIAL_HOUSING = "SOCIAL_HOUSING", STANDARD = "STANDARD", WATER = "WATER", - WINE = "WINE", + WINE = "WINE" } export enum VoucherDiscountType { FIXED = "FIXED", PERCENTAGE = "PERCENTAGE", - SHIPPING = "SHIPPING", + SHIPPING = "SHIPPING" } export enum VoucherTypeEnum { ENTIRE_ORDER = "ENTIRE_ORDER", SHIPPING = "SHIPPING", - SPECIFIC_PRODUCT = "SPECIFIC_PRODUCT", + SPECIFIC_PRODUCT = "SPECIFIC_PRODUCT" } export enum WebhookErrorCode { @@ -544,7 +546,7 @@ export enum WebhookErrorCode { INVALID = "INVALID", NOT_FOUND = "NOT_FOUND", REQUIRED = "REQUIRED", - UNIQUE = "UNIQUE", + UNIQUE = "UNIQUE" } export enum WebhookEventTypeEnum { @@ -555,14 +557,14 @@ export enum WebhookEventTypeEnum { ORDER_FULFILLED = "ORDER_FULFILLED", ORDER_FULLY_PAID = "ORDER_FULLY_PAID", ORDER_UPDATED = "ORDER_UPDATED", - PRODUCT_CREATED = "PRODUCT_CREATED", + PRODUCT_CREATED = "PRODUCT_CREATED" } export enum WeightUnitsEnum { G = "G", KG = "KG", LB = "LB", - OZ = "OZ", + OZ = "OZ" } export interface AddressInput {