0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

fix(console): avoid skipping m2m role assignment after switching browser tabs (#5973)

This commit is contained in:
Xiao Yijun 2024-06-03 14:34:07 +08:00 committed by GitHub
parent 6fbba3821d
commit 391717637c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 77 additions and 55 deletions

View file

@ -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);
})
);

View file

@ -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<string>('');
const [filterCategories, setFilterCategories] = useState<AppGuideCategory[]>([]);
const [selectedGuide, setSelectedGuide] = useState<SelectedGuide>();
const { getFilteredAppGuideMetadata, getStructuredAppGuideMetadata } = useAppGuideMetadata();
const [showCreateForm, setShowCreateForm] = useState<boolean>(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) {
)}
</div>
</div>
{selectedGuide?.target !== 'API' && showCreateForm && (
<ApplicationCreation
defaultCreateType={selectedGuide?.target}
defaultCreateFrameworkName={selectedGuide?.name}
isDefaultCreateThirdParty={selectedGuide?.isThirdParty}
onCompleted={onAppCreationCompleted}
/>
)}
</OverlayScrollbar>
);
}

View file

@ -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<SelectedGuide>) => void;
};
function GuideLibraryModal({ isOpen, onClose }: Props) {
const { navigate } = useTenantPathname();
const [showCreateForm, setShowCreateForm] = useState(false);
function GuideLibraryModal({ isOpen, onClose, onSelectGuide }: Props) {
return (
<Modal
shouldCloseOnEsc
@ -36,26 +41,17 @@ function GuideLibraryModal({ isOpen, onClose }: Props) {
requestSuccessMessage="guide.request_guide_successfully"
onClose={onClose}
/>
<GuideLibrary hasCardButton className={styles.content} />
<GuideLibrary hasCardButton className={styles.content} onSelectGuide={onSelectGuide} />
<ModalFooter
wrapperClassName={styles.footerInnerWrapper}
content="guide.do_not_need_tutorial"
buttonText="guide.app.continue_without_framework"
onClick={() => {
setShowCreateForm(true);
// Create application without a framework
onSelectGuide(null);
}}
/>
</div>
{showCreateForm && (
<ApplicationCreation
onCompleted={(newApp) => {
if (newApp) {
navigate(`/applications/${newApp.id}`);
}
setShowCreateForm(false);
}}
/>
)}
</Modal>
);
}

View file

@ -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<Nullable<SelectedGuide>>();
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 (
<div className={pageLayout.container}>
<PageMeta titleKey="applications.title" />
@ -122,7 +158,12 @@ function Applications({ tab }: Props) {
title="guide.app.select_framework_or_tutorial"
subtitle="guide.app.modal_subtitle"
/>
<GuideLibrary hasCardBorder hasCardButton className={styles.library} />
<GuideLibrary
hasCardBorder
hasCardButton
className={styles.library}
onSelectGuide={setSelectedGuide}
/>
</div>
)}
{(isLoading || !!applications?.length) && (
@ -179,7 +220,16 @@ function Applications({ tab }: Props) {
onClose={() => {
navigate(-1);
}}
onSelectGuide={setSelectedGuide}
/>
{selectedGuide !== undefined && (
<ApplicationCreation
defaultCreateType={cond(selectedGuide?.target !== 'API' && selectedGuide?.target)}
defaultCreateFrameworkName={selectedGuide?.name ?? undefined}
isDefaultCreateThirdParty={selectedGuide?.isThirdParty ?? undefined}
onCompleted={onAppCreationCompleted}
/>
)}
</div>
);
}