mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -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 { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Modal from 'react-modal';
|
import Modal from 'react-modal';
|
||||||
|
import { useSWRConfig } from 'swr';
|
||||||
|
|
||||||
import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
|
import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
|
||||||
import DynamicT from '@/ds-components/DynamicT';
|
import DynamicT from '@/ds-components/DynamicT';
|
||||||
|
@ -52,6 +53,7 @@ function CreateForm({
|
||||||
defaultValues: { type: defaultCreateType, isThirdParty: isDefaultCreateThirdParty },
|
defaultValues: { type: defaultCreateType, isThirdParty: isDefaultCreateThirdParty },
|
||||||
});
|
});
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
const { mutate: mutateGlobal } = useSWRConfig();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
field: { onChange, value, name, ref },
|
field: { onChange, value, name, ref },
|
||||||
|
@ -77,6 +79,8 @@ function CreateForm({
|
||||||
reportConversion({ gtagId: GtagConversionId.CreateFirstApp, transactionId: user?.id });
|
reportConversion({ gtagId: GtagConversionId.CreateFirstApp, transactionId: user?.id });
|
||||||
|
|
||||||
toast.success(t('applications.application_created'));
|
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);
|
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 { cond } from '@silverhand/essentials';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import SearchIcon from '@/assets/icons/search.svg';
|
import SearchIcon from '@/assets/icons/search.svg';
|
||||||
import ApplicationCreation from '@/components/ApplicationCreation';
|
|
||||||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
||||||
import FeatureTag from '@/components/FeatureTag';
|
import FeatureTag from '@/components/FeatureTag';
|
||||||
import { type SelectedGuide } from '@/components/Guide/GuideCard';
|
import { type SelectedGuide } from '@/components/Guide/GuideCard';
|
||||||
|
@ -18,7 +17,6 @@ import { CheckboxGroup } from '@/ds-components/Checkbox';
|
||||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||||
import TextInput from '@/ds-components/TextInput';
|
import TextInput from '@/ds-components/TextInput';
|
||||||
import TextLink from '@/ds-components/TextLink';
|
import TextLink from '@/ds-components/TextLink';
|
||||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
|
||||||
import { allAppGuideCategories, type AppGuideCategory } from '@/types/applications';
|
import { allAppGuideCategories, type AppGuideCategory } from '@/types/applications';
|
||||||
import { thirdPartyAppCategory } from '@/types/applications';
|
import { thirdPartyAppCategory } from '@/types/applications';
|
||||||
|
|
||||||
|
@ -30,17 +28,15 @@ type Props = {
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
readonly hasCardBorder?: boolean;
|
readonly hasCardBorder?: boolean;
|
||||||
readonly hasCardButton?: 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 { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { navigate } = useTenantPathname();
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const [keyword, setKeyword] = useState<string>('');
|
const [keyword, setKeyword] = useState<string>('');
|
||||||
const [filterCategories, setFilterCategories] = useState<AppGuideCategory[]>([]);
|
const [filterCategories, setFilterCategories] = useState<AppGuideCategory[]>([]);
|
||||||
const [selectedGuide, setSelectedGuide] = useState<SelectedGuide>();
|
|
||||||
const { getFilteredAppGuideMetadata, getStructuredAppGuideMetadata } = useAppGuideMetadata();
|
const { getFilteredAppGuideMetadata, getStructuredAppGuideMetadata } = useAppGuideMetadata();
|
||||||
const [showCreateForm, setShowCreateForm] = useState<boolean>(false);
|
|
||||||
const isApplicationCreateModal = pathname.includes('/applications/create');
|
const isApplicationCreateModal = pathname.includes('/applications/create');
|
||||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||||
|
|
||||||
|
@ -65,27 +61,11 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton }: Props) {
|
||||||
);
|
);
|
||||||
}, [isApplicationCreateModal]);
|
}, [isApplicationCreateModal]);
|
||||||
|
|
||||||
const onClickGuide = useCallback((data: SelectedGuide) => {
|
const onClickGuide = useCallback(
|
||||||
setShowCreateForm(true);
|
(data: SelectedGuide) => {
|
||||||
setSelectedGuide(data);
|
onSelectGuide(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);
|
|
||||||
},
|
},
|
||||||
[navigate, selectedGuide]
|
[onSelectGuide]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -184,14 +164,6 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton }: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedGuide?.target !== 'API' && showCreateForm && (
|
|
||||||
<ApplicationCreation
|
|
||||||
defaultCreateType={selectedGuide?.target}
|
|
||||||
defaultCreateFrameworkName={selectedGuide?.name}
|
|
||||||
isDefaultCreateThirdParty={selectedGuide?.isThirdParty}
|
|
||||||
onCompleted={onAppCreationCompleted}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { useState } from 'react';
|
import { type Nullable } from '@silverhand/essentials';
|
||||||
import Modal from 'react-modal';
|
import Modal from 'react-modal';
|
||||||
|
|
||||||
import ApplicationCreation from '@/components/ApplicationCreation';
|
import { type SelectedGuide } from '@/components/Guide/GuideCard';
|
||||||
import ModalFooter from '@/components/Guide/ModalFooter';
|
import ModalFooter from '@/components/Guide/ModalFooter';
|
||||||
import ModalHeader from '@/components/Guide/ModalHeader';
|
import ModalHeader from '@/components/Guide/ModalHeader';
|
||||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
|
||||||
import * as modalStyles from '@/scss/modal.module.scss';
|
import * as modalStyles from '@/scss/modal.module.scss';
|
||||||
|
|
||||||
import GuideLibrary from '../GuideLibrary';
|
import GuideLibrary from '../GuideLibrary';
|
||||||
|
@ -14,11 +13,17 @@ import * as styles from './index.module.scss';
|
||||||
type Props = {
|
type Props = {
|
||||||
readonly isOpen: boolean;
|
readonly isOpen: boolean;
|
||||||
readonly onClose: () => void;
|
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) {
|
function GuideLibraryModal({ isOpen, onClose, onSelectGuide }: Props) {
|
||||||
const { navigate } = useTenantPathname();
|
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
shouldCloseOnEsc
|
shouldCloseOnEsc
|
||||||
|
@ -36,26 +41,17 @@ function GuideLibraryModal({ isOpen, onClose }: Props) {
|
||||||
requestSuccessMessage="guide.request_guide_successfully"
|
requestSuccessMessage="guide.request_guide_successfully"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
<GuideLibrary hasCardButton className={styles.content} />
|
<GuideLibrary hasCardButton className={styles.content} onSelectGuide={onSelectGuide} />
|
||||||
<ModalFooter
|
<ModalFooter
|
||||||
wrapperClassName={styles.footerInnerWrapper}
|
wrapperClassName={styles.footerInnerWrapper}
|
||||||
content="guide.do_not_need_tutorial"
|
content="guide.do_not_need_tutorial"
|
||||||
buttonText="guide.app.continue_without_framework"
|
buttonText="guide.app.continue_without_framework"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCreateForm(true);
|
// Create application without a framework
|
||||||
|
onSelectGuide(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showCreateForm && (
|
|
||||||
<ApplicationCreation
|
|
||||||
onCompleted={(newApp) => {
|
|
||||||
if (newApp) {
|
|
||||||
navigate(`/applications/${newApp.id}`);
|
|
||||||
}
|
|
||||||
setShowCreateForm(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Modal>
|
</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 { useTranslation } from 'react-i18next';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import Plus from '@/assets/icons/plus.svg';
|
import Plus from '@/assets/icons/plus.svg';
|
||||||
|
import ApplicationCreation from '@/components/ApplicationCreation';
|
||||||
import ApplicationIcon from '@/components/ApplicationIcon';
|
import ApplicationIcon from '@/components/ApplicationIcon';
|
||||||
import ChargeNotification from '@/components/ChargeNotification';
|
import ChargeNotification from '@/components/ChargeNotification';
|
||||||
|
import { type SelectedGuide } from '@/components/Guide/GuideCard';
|
||||||
import ItemPreview from '@/components/ItemPreview';
|
import ItemPreview from '@/components/ItemPreview';
|
||||||
import PageMeta from '@/components/PageMeta';
|
import PageMeta from '@/components/PageMeta';
|
||||||
import { isCloud } from '@/consts/env';
|
import { isCloud } from '@/consts/env';
|
||||||
|
@ -52,6 +56,13 @@ function Applications({ tab }: Props) {
|
||||||
|
|
||||||
const isCreating = match(createApplicationPathname);
|
const isCreating = match(createApplicationPathname);
|
||||||
const { hasMachineToMachineAppsSurpassedLimit } = useApplicationsUsage();
|
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 {
|
const {
|
||||||
data,
|
data,
|
||||||
|
@ -66,6 +77,31 @@ function Applications({ tab }: Props) {
|
||||||
const isLoading = !data && !error;
|
const isLoading = !data && !error;
|
||||||
const [applications, totalCount] = data ?? [];
|
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 (
|
return (
|
||||||
<div className={pageLayout.container}>
|
<div className={pageLayout.container}>
|
||||||
<PageMeta titleKey="applications.title" />
|
<PageMeta titleKey="applications.title" />
|
||||||
|
@ -122,7 +158,12 @@ function Applications({ tab }: Props) {
|
||||||
title="guide.app.select_framework_or_tutorial"
|
title="guide.app.select_framework_or_tutorial"
|
||||||
subtitle="guide.app.modal_subtitle"
|
subtitle="guide.app.modal_subtitle"
|
||||||
/>
|
/>
|
||||||
<GuideLibrary hasCardBorder hasCardButton className={styles.library} />
|
<GuideLibrary
|
||||||
|
hasCardBorder
|
||||||
|
hasCardButton
|
||||||
|
className={styles.library}
|
||||||
|
onSelectGuide={setSelectedGuide}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(isLoading || !!applications?.length) && (
|
{(isLoading || !!applications?.length) && (
|
||||||
|
@ -179,7 +220,16 @@ function Applications({ tab }: Props) {
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
navigate(-1);
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue