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:
parent
6fbba3821d
commit
391717637c
4 changed files with 77 additions and 55 deletions
|
@ -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);
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue