diff --git a/src/components/ControlledSwitch.tsx b/src/components/ControlledSwitch.tsx index e7dea9c2a..d38bcda2b 100644 --- a/src/components/ControlledSwitch.tsx +++ b/src/components/ControlledSwitch.tsx @@ -4,10 +4,7 @@ import Switch from "@material-ui/core/Switch"; import React from "react"; const useStyles = makeStyles( - theme => ({ - label: { - marginLeft: theme.spacing(3.5) - }, + () => ({ labelText: { fontSize: 14 } @@ -51,7 +48,7 @@ export const ControlledSwitch: React.FC = props => { /> } label={ -
+
{uncheckedLabel ? ( checked ? ( label diff --git a/src/fragments/errors.ts b/src/fragments/errors.ts index fda532572..538e310b5 100644 --- a/src/fragments/errors.ts +++ b/src/fragments/errors.ts @@ -127,3 +127,10 @@ export const exportErrorFragment = gql` field } `; + +export const pluginErrorFragment = gql` + fragment PluginErrorFragment on PluginError { + code + field + } +`; diff --git a/src/fragments/types/PluginErrorFragment.ts b/src/fragments/types/PluginErrorFragment.ts new file mode 100644 index 000000000..ce557f60e --- /dev/null +++ b/src/fragments/types/PluginErrorFragment.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PluginErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL fragment: PluginErrorFragment +// ==================================================== + +export interface PluginErrorFragment { + __typename: "PluginError"; + code: PluginErrorCode; + field: string | null; +} diff --git a/src/plugins/components/PluginInfo/PluginInfo.tsx b/src/plugins/components/PluginInfo/PluginInfo.tsx index 794d09297..861c6c8eb 100644 --- a/src/plugins/components/PluginInfo/PluginInfo.tsx +++ b/src/plugins/components/PluginInfo/PluginInfo.tsx @@ -6,7 +6,10 @@ import CardTitle from "@saleor/components/CardTitle"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; import FormSpacer from "@saleor/components/FormSpacer"; import Hr from "@saleor/components/Hr"; +import { PluginErrorFragment } from "@saleor/fragments/types/PluginErrorFragment"; import { commonMessages } from "@saleor/intl"; +import { PluginErrorCode } from "@saleor/types/globalTypes"; +import getPluginErrorMessage from "@saleor/utils/errors/plugins"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -15,6 +18,7 @@ import { FormData } from "../PluginsDetailsPage"; interface PluginInfoProps { data: FormData; description: string; + errors: PluginErrorFragment[]; name: string; onChange: (event: React.ChangeEvent) => void; } @@ -35,11 +39,17 @@ const useStyles = makeStyles( const PluginInfo: React.FC = ({ data, description, + errors, name, onChange }) => { const classes = useStyles({}); const intl = useIntl(); + + const misconfiguredError = errors.find( + err => err.code === PluginErrorCode.PLUGIN_MISCONFIGURED + ); + return ( = ({ checked={data.active} onChange={onChange} /> + {misconfiguredError && ( + + {getPluginErrorMessage(misconfiguredError, intl)} + + )} ); diff --git a/src/plugins/components/PluginSettings/PluginSettings.tsx b/src/plugins/components/PluginSettings/PluginSettings.tsx index 015146edd..b470ca9c4 100644 --- a/src/plugins/components/PluginSettings/PluginSettings.tsx +++ b/src/plugins/components/PluginSettings/PluginSettings.tsx @@ -2,8 +2,11 @@ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import makeStyles from "@material-ui/core/styles/makeStyles"; import TextField from "@material-ui/core/TextField"; +import Tooltip from "@material-ui/core/Tooltip"; +import Typography from "@material-ui/core/Typography"; +import InfoIcon from "@material-ui/icons/Info"; import CardTitle from "@saleor/components/CardTitle"; -import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; +import ControlledSwitch from "@saleor/components/ControlledSwitch"; import { Plugin_plugin_configuration } from "@saleor/plugins/types/Plugin"; import { UserError } from "@saleor/types"; import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes"; @@ -32,7 +35,9 @@ const useStyles = makeStyles( item: { "&:not(:last-child)": { marginBottom: theme.spacing(3) - } + }, + alignItems: "center", + display: "flex" }, itemLabel: { fontWeight: 500 @@ -71,17 +76,30 @@ const PluginSettings: React.FC = ({ return (
{fieldData.type === ConfigurationTypeFieldEnum.BOOLEAN ? ( - + <> + + {fieldData.helpText && ( + + {fieldData.helpText} + + } + > + + + )} + ) : ( void; @@ -65,15 +65,13 @@ const PluginsDetailsPage: React.FC = props => { const classes = useStyles(props); const intl = useIntl(); const initialForm: FormData = { - active: maybe(() => plugin.active, false), - configuration: maybe(() => - plugin.configuration - .filter(field => !isSecretField(plugin.configuration, field.name)) - .map(field => ({ - ...field, - value: field.value || "" - })) - ) + active: plugin?.active || false, + configuration: plugin?.configuration + ?.filter(field => !isSecretField(plugin?.configuration || [], field.name)) + .map(field => ({ + ...field, + value: field.value || "" + })) }; return ( @@ -108,7 +106,7 @@ const PluginsDetailsPage: React.FC = props => { description: "header" }, { - pluginName: maybe(() => plugin.name, "...") + pluginName: getStringOrPlaceholder(plugin?.name) } )} /> @@ -129,8 +127,9 @@ const PluginsDetailsPage: React.FC = props => {
plugin.description, "")} - name={maybe(() => plugin.name, "")} + description={plugin?.description || ""} + errors={errors} + name={plugin?.name || ""} onChange={onChange} /> {data.configuration && ( @@ -143,25 +142,17 @@ const PluginsDetailsPage: React.FC = props => { description: "section header" })} - - {intl.formatMessage({ - defaultMessage: - "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." - })} -
plugin.configuration, [])} + fields={plugin?.configuration || []} errors={errors} disabled={disabled} onChange={onChange} /> - {maybe(() => - plugin.configuration.some(field => - isSecretField(plugin.configuration, field.name) - ) + {plugin?.configuration.some(field => + isSecretField(plugin.configuration, field.name) ) && ( <> diff --git a/src/plugins/mutations.ts b/src/plugins/mutations.ts index 0d073c06e..ce6419de5 100644 --- a/src/plugins/mutations.ts +++ b/src/plugins/mutations.ts @@ -1,3 +1,4 @@ +import { pluginErrorFragment } from "@saleor/fragments/errors"; import { pluginsDetailsFragment } from "@saleor/fragments/plugins"; import gql from "graphql-tag"; @@ -6,11 +7,11 @@ import { PluginUpdate, PluginUpdateVariables } from "./types/PluginUpdate"; const pluginUpdate = gql` ${pluginsDetailsFragment} + ${pluginErrorFragment} mutation PluginUpdate($id: ID!, $input: PluginUpdateInput!) { pluginUpdate(id: $id, input: $input) { - errors { - field - message + errors: pluginsErrors { + ...PluginErrorFragment } plugin { ...PluginsDetailsFragment diff --git a/src/plugins/types/PluginUpdate.ts b/src/plugins/types/PluginUpdate.ts index c420bf0b0..6aa937c5a 100644 --- a/src/plugins/types/PluginUpdate.ts +++ b/src/plugins/types/PluginUpdate.ts @@ -2,16 +2,16 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { PluginUpdateInput, ConfigurationTypeFieldEnum } from "./../../types/globalTypes"; +import { PluginUpdateInput, PluginErrorCode, ConfigurationTypeFieldEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: PluginUpdate // ==================================================== export interface PluginUpdate_pluginUpdate_errors { - __typename: "Error"; + __typename: "PluginError"; + code: PluginErrorCode; field: string | null; - message: string | null; } export interface PluginUpdate_pluginUpdate_plugin_configuration { diff --git a/src/plugins/views/PluginsDetails.tsx b/src/plugins/views/PluginsDetails.tsx index 545fe1982..74b719024 100644 --- a/src/plugins/views/PluginsDetails.tsx +++ b/src/plugins/views/PluginsDetails.tsx @@ -9,7 +9,6 @@ import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandl import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { maybe } from "../../misc"; import PluginsDetailsPage from "../components/PluginsDetailsPage"; import PluginSecretFieldDialog from "../components/PluginSecretFieldDialog"; import { TypedPluginUpdate } from "../mutations"; @@ -73,10 +72,7 @@ export const PluginsDetails: React.FC = ({ {pluginDetails => ( {(pluginUpdate, pluginUpdateOpts) => { - const formErrors = maybe( - () => pluginUpdateOpts.data.pluginUpdate.errors, - [] - ); + const formErrors = pluginUpdateOpts.data?.pluginUpdate.errors || []; const handleFieldUpdate = (value: string) => pluginUpdate({ @@ -95,16 +91,14 @@ export const PluginsDetails: React.FC = ({ return ( <> - pluginDetails.data.plugin.name)} - /> + pluginDetails.data.plugin)} + plugin={pluginDetails.data?.plugin} onBack={() => navigate(pluginListUrl())} onClear={id => openModal("clear", { @@ -131,7 +125,7 @@ export const PluginsDetails: React.FC = ({ }) } /> - {maybe(() => pluginDetails.data.plugin.configuration) && ( + {pluginDetails.data?.plugin?.configuration && ( <> = ({ confirmButtonState={ !!params.action ? pluginUpdateOpts.status : "default" } - field={maybe(() => - pluginDetails.data.plugin.configuration.find( - field => field.name === params.id - ) + field={pluginDetails.data?.plugin?.configuration.find( + field => field.name === params.id )} onClose={closeModal} onConfirm={formData => handleFieldUpdate(formData.value)} diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 085d7b826..d6687fcb9 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -111209,11 +111209,6 @@ exports[`Storyshots Views / Plugins / Plugin details default 1`] = ` > Plugin Settings
-
- 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. -
- - + + - Use sandbox +
+ + Use sandbox + +
+
+
@@ -111660,6 +111661,11 @@ exports[`Storyshots Views / Plugins / Plugin details form errors 1`] = ` Set plugin as Active +
+ Plugin is misconfigured and cannot be activated +

Plugin Settings -
- 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. -
- - + + - Use sandbox +
+ + Use sandbox + +
+
+
@@ -114712,9 +114719,7 @@ exports[`Storyshots Views / Product types / Product type details default 1`] = ` -
+
@@ -115469,9 +115474,7 @@ exports[`Storyshots Views / Product types / Product type details form errors 1`] -
+
@@ -116013,9 +116016,7 @@ exports[`Storyshots Views / Product types / Product type details loading 1`] = ` -
+
@@ -116396,9 +116397,7 @@ exports[`Storyshots Views / Product types / Product type details no attributes 1 -
+
diff --git a/src/storybook/stories/plugins/PluginDetailsPage.tsx b/src/storybook/stories/plugins/PluginDetailsPage.tsx index 61bb17e7a..3a79bbc76 100644 --- a/src/storybook/stories/plugins/PluginDetailsPage.tsx +++ b/src/storybook/stories/plugins/PluginDetailsPage.tsx @@ -1,3 +1,4 @@ +import { PluginErrorCode } from "@saleor/types/globalTypes"; import { storiesOf } from "@storybook/react"; import React from "react"; @@ -7,7 +8,6 @@ import PluginsDetailsPage, { } from "../../../plugins/components/PluginsDetailsPage"; import { plugin } from "../../../plugins/fixtures"; import Decorator from "../../Decorator"; -import { formError } from "../../misc"; const props: PluginsDetailsPageProps = { disabled: false, @@ -29,11 +29,20 @@ storiesOf("Views / Plugins / Plugin details", module) .add("form errors", () => ( ).map(formError)} + errors={[ + ...(["active", "Username or account", "Password or license"] as Array< + keyof FormData + >).map(field => ({ + __typename: "PluginError" as "PluginError", + code: PluginErrorCode.INVALID, + field + })), + { + __typename: "PluginError" as "PluginError", + code: PluginErrorCode.PLUGIN_MISCONFIGURED, + field: null + } + ]} /> )) .add("not configurable", () => ( diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index d2949a01f..ddcd0567a 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -665,6 +665,15 @@ export enum PermissionGroupSortField { NAME = "NAME", } +export enum PluginErrorCode { + GRAPHQL_ERROR = "GRAPHQL_ERROR", + INVALID = "INVALID", + NOT_FOUND = "NOT_FOUND", + PLUGIN_MISCONFIGURED = "PLUGIN_MISCONFIGURED", + REQUIRED = "REQUIRED", + UNIQUE = "UNIQUE", +} + export enum PluginSortField { IS_ACTIVE = "IS_ACTIVE", NAME = "NAME", diff --git a/src/utils/errors/plugins.ts b/src/utils/errors/plugins.ts new file mode 100644 index 000000000..7d79cbf64 --- /dev/null +++ b/src/utils/errors/plugins.ts @@ -0,0 +1,41 @@ +import { PluginErrorFragment } from "@saleor/fragments/types/PluginErrorFragment"; +import { commonMessages } from "@saleor/intl"; +import { PluginErrorCode } from "@saleor/types/globalTypes"; +import { defineMessages, IntlShape } from "react-intl"; + +import commonErrorMessages from "./common"; + +const messages = defineMessages({ + misconfigured: { + defaultMessage: "Plugin is misconfigured and cannot be activated" + }, + unique: { + defaultMessage: "This field needs to be unique" + } +}); + +function getPluginErrorMessage( + err: PluginErrorFragment, + intl: IntlShape +): string { + if (err) { + switch (err.code) { + case PluginErrorCode.PLUGIN_MISCONFIGURED: + return intl.formatMessage(messages.misconfigured); + case PluginErrorCode.GRAPHQL_ERROR: + return intl.formatMessage(commonErrorMessages.graphqlError); + case PluginErrorCode.INVALID: + return intl.formatMessage(commonErrorMessages.invalid); + case PluginErrorCode.REQUIRED: + return intl.formatMessage(commonMessages.requiredField); + case PluginErrorCode.UNIQUE: + return intl.formatMessage(messages.unique); + default: + return intl.formatMessage(commonErrorMessages.unknownError); + } + } + + return undefined; +} + +export default getPluginErrorMessage;