From 391717637c36ff1dfb7f5dcaa50fcf0f1c57fe17 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 3 Jun 2024 14:34:07 +0800 Subject: [PATCH] fix(console): avoid skipping m2m role assignment after switching browser tabs (#5973) --- .../ApplicationCreation/CreateForm/index.tsx | 4 ++ .../components/GuideLibrary/index.tsx | 42 +++------------ .../components/GuideLibraryModal/index.tsx | 32 +++++------ .../console/src/pages/Applications/index.tsx | 54 ++++++++++++++++++- 4 files changed, 77 insertions(+), 55 deletions(-) diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx index ada8a7984..21c88536a 100644 --- a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx +++ b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx @@ -6,6 +6,7 @@ import { useController, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import Modal from 'react-modal'; +import { useSWRConfig } from 'swr'; import { GtagConversionId, reportConversion } from '@/components/Conversion/utils'; import DynamicT from '@/ds-components/DynamicT'; @@ -52,6 +53,7 @@ function CreateForm({ defaultValues: { type: defaultCreateType, isThirdParty: isDefaultCreateThirdParty }, }); const { user } = useCurrentUser(); + const { mutate: mutateGlobal } = useSWRConfig(); const { field: { onChange, value, name, ref }, @@ -77,6 +79,8 @@ function CreateForm({ reportConversion({ gtagId: GtagConversionId.CreateFirstApp, transactionId: user?.id }); toast.success(t('applications.application_created')); + // Trigger a refetch of the applications list + void mutateGlobal((key) => typeof key === 'string' && key.startsWith('api/applications')); onClose?.(createdApp); }) ); diff --git a/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx b/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx index 6b5e4e3d9..daa96d3b3 100644 --- a/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx @@ -1,4 +1,4 @@ -import { ApplicationType, type Application, ReservedPlanId } from '@logto/schemas'; +import { ApplicationType, ReservedPlanId } from '@logto/schemas'; import { cond } from '@silverhand/essentials'; import classNames from 'classnames'; import { useCallback, useContext, useMemo, useState } from 'react'; @@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import SearchIcon from '@/assets/icons/search.svg'; -import ApplicationCreation from '@/components/ApplicationCreation'; import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder'; import FeatureTag from '@/components/FeatureTag'; import { type SelectedGuide } from '@/components/Guide/GuideCard'; @@ -18,7 +17,6 @@ import { CheckboxGroup } from '@/ds-components/Checkbox'; import OverlayScrollbar from '@/ds-components/OverlayScrollbar'; import TextInput from '@/ds-components/TextInput'; import TextLink from '@/ds-components/TextLink'; -import useTenantPathname from '@/hooks/use-tenant-pathname'; import { allAppGuideCategories, type AppGuideCategory } from '@/types/applications'; import { thirdPartyAppCategory } from '@/types/applications'; @@ -30,17 +28,15 @@ type Props = { readonly className?: string; readonly hasCardBorder?: boolean; readonly hasCardButton?: boolean; + readonly onSelectGuide: (data?: SelectedGuide) => void; }; -function GuideLibrary({ className, hasCardBorder, hasCardButton }: Props) { +function GuideLibrary({ className, hasCardBorder, hasCardButton, onSelectGuide }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { navigate } = useTenantPathname(); const { pathname } = useLocation(); const [keyword, setKeyword] = useState(''); const [filterCategories, setFilterCategories] = useState([]); - const [selectedGuide, setSelectedGuide] = useState(); const { getFilteredAppGuideMetadata, getStructuredAppGuideMetadata } = useAppGuideMetadata(); - const [showCreateForm, setShowCreateForm] = useState(false); const isApplicationCreateModal = pathname.includes('/applications/create'); const { currentPlan } = useContext(SubscriptionDataContext); @@ -65,27 +61,11 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton }: Props) { ); }, [isApplicationCreateModal]); - const onClickGuide = useCallback((data: SelectedGuide) => { - setShowCreateForm(true); - setSelectedGuide(data); - }, []); - - const onAppCreationCompleted = useCallback( - (newApp?: Application) => { - if (newApp && selectedGuide) { - navigate( - // Third party app directly goes to the app detail page - selectedGuide.isThirdParty - ? `/applications/${newApp.id}` - : `/applications/${newApp.id}/guide/${selectedGuide.id}`, - { replace: true } - ); - return; - } - setShowCreateForm(false); - setSelectedGuide(undefined); + const onClickGuide = useCallback( + (data: SelectedGuide) => { + onSelectGuide(data); }, - [navigate, selectedGuide] + [onSelectGuide] ); return ( @@ -184,14 +164,6 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton }: Props) { )} - {selectedGuide?.target !== 'API' && showCreateForm && ( - - )} ); } diff --git a/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx b/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx index 2facd58c8..345665363 100644 --- a/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx @@ -1,10 +1,9 @@ -import { useState } from 'react'; +import { type Nullable } from '@silverhand/essentials'; import Modal from 'react-modal'; -import ApplicationCreation from '@/components/ApplicationCreation'; +import { type SelectedGuide } from '@/components/Guide/GuideCard'; import ModalFooter from '@/components/Guide/ModalFooter'; import ModalHeader from '@/components/Guide/ModalHeader'; -import useTenantPathname from '@/hooks/use-tenant-pathname'; import * as modalStyles from '@/scss/modal.module.scss'; import GuideLibrary from '../GuideLibrary'; @@ -14,11 +13,17 @@ import * as styles from './index.module.scss'; type Props = { readonly isOpen: boolean; readonly onClose: () => void; + /** + * The callback function when a guide is selected + * For the parameter: + * - `undefined`: No guide is selected + * - `null`: Create application without a framework + * - `selectedGuide`: The selected guide + */ + readonly onSelectGuide: (guide?: Nullable) => void; }; -function GuideLibraryModal({ isOpen, onClose }: Props) { - const { navigate } = useTenantPathname(); - const [showCreateForm, setShowCreateForm] = useState(false); +function GuideLibraryModal({ isOpen, onClose, onSelectGuide }: Props) { return ( - + { - setShowCreateForm(true); + // Create application without a framework + onSelectGuide(null); }} /> - {showCreateForm && ( - { - if (newApp) { - navigate(`/applications/${newApp.id}`); - } - setShowCreateForm(false); - }} - /> - )} ); } diff --git a/packages/console/src/pages/Applications/index.tsx b/packages/console/src/pages/Applications/index.tsx index b92d05589..f063a09bb 100644 --- a/packages/console/src/pages/Applications/index.tsx +++ b/packages/console/src/pages/Applications/index.tsx @@ -1,10 +1,14 @@ -import { joinPath } from '@silverhand/essentials'; +import { type Application } from '@logto/schemas'; +import { type Nullable, joinPath, cond } from '@silverhand/essentials'; +import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import Plus from '@/assets/icons/plus.svg'; +import ApplicationCreation from '@/components/ApplicationCreation'; import ApplicationIcon from '@/components/ApplicationIcon'; import ChargeNotification from '@/components/ChargeNotification'; +import { type SelectedGuide } from '@/components/Guide/GuideCard'; import ItemPreview from '@/components/ItemPreview'; import PageMeta from '@/components/PageMeta'; import { isCloud } from '@/consts/env'; @@ -52,6 +56,13 @@ function Applications({ tab }: Props) { const isCreating = match(createApplicationPathname); const { hasMachineToMachineAppsSurpassedLimit } = useApplicationsUsage(); + /** + * Selected guide from the guide library + * - `undefined`: No guide is selected + * - `null`: Create application without a framework guide + * - `selectedGuide`: Create application with the selected guide + */ + const [selectedGuide, setSelectedGuide] = useState>(); const { data, @@ -66,6 +77,31 @@ function Applications({ tab }: Props) { const isLoading = !data && !error; const [applications, totalCount] = data ?? []; + const onAppCreationCompleted = useCallback( + (newApp?: Application) => { + if (newApp) { + /** + * Navigate to the application details page if no framework guide is selected or the selected guide is third party + */ + if (selectedGuide === null || selectedGuide?.isThirdParty) { + navigate(`/applications/${newApp.id}`, { replace: true }); + setSelectedGuide(undefined); + return; + } + + // Create application from the framework guide + if (selectedGuide) { + navigate(`/applications/${newApp.id}/guide/${selectedGuide.id}`, { replace: true }); + setSelectedGuide(undefined); + return; + } + } + + setSelectedGuide(undefined); + }, + [navigate, selectedGuide] + ); + return (
@@ -122,7 +158,12 @@ function Applications({ tab }: Props) { title="guide.app.select_framework_or_tutorial" subtitle="guide.app.modal_subtitle" /> - +
)} {(isLoading || !!applications?.length) && ( @@ -179,7 +220,16 @@ function Applications({ tab }: Props) { onClose={() => { navigate(-1); }} + onSelectGuide={setSelectedGuide} /> + {selectedGuide !== undefined && ( + + )} ); }