From 78407fc6c9d8a18d8253e0052c63ea1dd63de576 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Wed, 6 Jul 2022 16:39:41 +0800 Subject: [PATCH] feat(console): add unsaved changes alert for connector config (#1414) --- .../components/ConnectorContent.tsx | 126 ++++++++++++++++++ .../src/pages/ConnectorDetails/index.tsx | 82 ++---------- 2 files changed, 135 insertions(+), 73 deletions(-) create mode 100644 packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx diff --git a/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx b/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx new file mode 100644 index 000000000..efd00699f --- /dev/null +++ b/packages/console/src/pages/ConnectorDetails/components/ConnectorContent.tsx @@ -0,0 +1,126 @@ +import { Connector, ConnectorDTO, ConnectorMetadata, ConnectorType } from '@logto/schemas'; +import React, { useEffect, useMemo, useState } from 'react'; +import { toast } from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; + +import Button from '@/components/Button'; +import CodeEditor from '@/components/CodeEditor'; +import FormField from '@/components/FormField'; +import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; +import useApi from '@/hooks/use-api'; +import * as detailsStyles from '@/scss/details.module.scss'; + +import * as styles from '../index.module.scss'; +import SenderTester from './SenderTester'; + +type Props = { + connectorData: ConnectorDTO; + onConnectorUpdated: (connector: ConnectorDTO) => void; +}; + +const ConnectorContent = ({ connectorData, onConnectorUpdated }: Props) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const [config, setConfig] = useState(); + const [saveError, setSaveError] = useState(); + const [isSubmitting, setIsSubmitting] = useState(false); + const api = useApi(); + + const defaultConfig = useMemo(() => { + const hasData = Object.keys(connectorData.config).length > 0; + + return hasData ? JSON.stringify(connectorData.config, null, 2) : connectorData.configTemplate; + }, [connectorData]); + + useEffect(() => { + setConfig(defaultConfig); + + return () => { + setConfig(defaultConfig); + }; + }, [defaultConfig]); + + const hasUnsavedChanges = useMemo(() => { + if (!config) { + return false; + } + + try { + const parsedConfig = JSON.stringify(JSON.parse(config), null, 2); + + return parsedConfig !== defaultConfig; + } catch { + return true; + } + }, [config, defaultConfig]); + + const handleSave = async () => { + setSaveError(undefined); + + if (!config) { + setSaveError(t('connector_details.save_error_empty_config')); + + return; + } + + try { + const configJson = JSON.parse(config) as JSON; + setIsSubmitting(true); + const { metadata, ...reset } = await api + .patch(`/api/connectors/${connectorData.id}`, { + json: { config: configJson }, + }) + .json< + Connector & { + metadata: ConnectorMetadata; + } + >(); + onConnectorUpdated({ ...reset, ...metadata }); + toast.success(t('general.saved')); + } catch (error: unknown) { + if (error instanceof SyntaxError) { + setSaveError(t('connector_details.save_error_json_parse_error')); + } + } + + setIsSubmitting(false); + }; + + return ( + <> +
+ + { + setConfig(value); + }} + /> + + {connectorData.type !== ConnectorType.Social && ( + + )} + {saveError &&
{saveError}
} +
+
+
+
+
+ + + ); +}; + +export default ConnectorContent; diff --git a/packages/console/src/pages/ConnectorDetails/index.tsx b/packages/console/src/pages/ConnectorDetails/index.tsx index 90dd6d7b6..c7f7df25a 100644 --- a/packages/console/src/pages/ConnectorDetails/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/index.tsx @@ -1,6 +1,6 @@ import { AppearanceMode, ConnectorDTO, ConnectorType } from '@logto/schemas'; import classNames from 'classnames'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; @@ -9,11 +9,9 @@ import useSWR from 'swr'; import ActionMenu, { ActionMenuItem } from '@/components/ActionMenu'; import Button from '@/components/Button'; import Card from '@/components/Card'; -import CodeEditor from '@/components/CodeEditor'; import CopyToClipboard from '@/components/CopyToClipboard'; import DetailsSkeleton from '@/components/DetailsSkeleton'; import Drawer from '@/components/Drawer'; -import FormField from '@/components/FormField'; import LinkButton from '@/components/LinkButton'; import Markdown from '@/components/Markdown'; import Status from '@/components/Status'; @@ -29,20 +27,17 @@ import Reset from '@/icons/Reset'; import * as detailsStyles from '@/scss/details.module.scss'; import CreateForm from '../Connectors/components/CreateForm'; +import ConnectorContent from './components/ConnectorContent'; import ConnectorTabs from './components/ConnectorTabs'; import ConnectorTypeName from './components/ConnectorTypeName'; -import SenderTester from './components/SenderTester'; import * as styles from './index.module.scss'; const ConnectorDetails = () => { const { connectorId } = useParams(); const [isReadMeOpen, setIsReadMeOpen] = useState(false); - const [config, setConfig] = useState(); - const [saveError, setSaveError] = useState(); - const [isSubmitting, setIsSubmitting] = useState(false); const [isSetupOpen, setIsSetupOpen] = useState(false); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { data, error } = useSWR( + const { data, error, mutate } = useSWR( connectorId && `/api/connectors/${connectorId}` ); const inUse = useConnectorInUse(data?.type, data?.target); @@ -51,44 +46,6 @@ const ConnectorDetails = () => { const navigate = useNavigate(); const theme = useTheme(); - useEffect(() => { - if (data) { - const hasData = Object.keys(data.config).length > 0; - setConfig(hasData ? JSON.stringify(data.config, null, 2) : data.configTemplate); - } - }, [data]); - - const handleSave = async () => { - if (!connectorId) { - return; - } - - setSaveError(undefined); - - if (!config) { - setSaveError(t('connector_details.save_error_empty_config')); - - return; - } - - try { - const configJson = JSON.parse(config) as JSON; - setIsSubmitting(true); - await api - .patch(`/api/connectors/${connectorId}`, { - json: { config: configJson }, - }) - .json(); - toast.success(t('general.saved')); - } catch (error: unknown) { - if (error instanceof SyntaxError) { - setSaveError(t('connector_details.save_error_json_parse_error')); - } - } - - setIsSubmitting(false); - }; - const handleDelete = async () => { if (!connectorId) { return; @@ -208,33 +165,12 @@ const ConnectorDetails = () => { {t('general.settings_nav')} -
- - { - setConfig(value); - }} - /> - - {data.type !== ConnectorType.Social && ( - - )} - {saveError &&
{saveError}
} -
-
-
-
-
+ { + void mutate(connector); + }} + /> )}