diff --git a/packages/console/src/hooks/use-console-routes/routes/api-resources.tsx b/packages/console/src/hooks/use-console-routes/routes/api-resources.tsx index 7b53fffa1..4e284d237 100644 --- a/packages/console/src/hooks/use-console-routes/routes/api-resources.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/api-resources.tsx @@ -1,6 +1,9 @@ -import { type RouteObject } from 'react-router-dom'; +import { Navigate, type RouteObject } from 'react-router-dom'; +import { ApiResourceDetailsTabs } from '@/consts'; import ApiResourceDetails from '@/pages/ApiResourceDetails'; +import ApiResourcePermissions from '@/pages/ApiResourceDetails/ApiResourcePermissions'; +import ApiResourceSettings from '@/pages/ApiResourceDetails/ApiResourceSettings'; import ApiResources from '@/pages/ApiResources'; export const apiResources: RouteObject = { @@ -9,6 +12,14 @@ export const apiResources: RouteObject = { { index: true, element: }, { path: 'create', element: }, { path: ':id/guide/:guideId', element: }, - { path: ':id/*', element: }, + { + path: ':id/*', + element: , + children: [ + { index: true, element: }, + { path: ApiResourceDetailsTabs.Permissions, element: }, + { path: ApiResourceDetailsTabs.General, element: }, + ], + }, ], }; diff --git a/packages/console/src/hooks/use-console-routes/routes/organization-template.tsx b/packages/console/src/hooks/use-console-routes/routes/organization-template.tsx index 52c26c61c..21d30eb6c 100644 --- a/packages/console/src/hooks/use-console-routes/routes/organization-template.tsx +++ b/packages/console/src/hooks/use-console-routes/routes/organization-template.tsx @@ -1,7 +1,9 @@ import { Navigate, type RouteObject } from 'react-router-dom'; -import { OrganizationTemplateTabs } from '@/consts'; +import { OrganizationRoleDetailsTabs, OrganizationTemplateTabs } from '@/consts'; import OrganizationRoleDetails from '@/pages/OrganizationRoleDetails'; +import Permissions from '@/pages/OrganizationRoleDetails/Permissions'; +import Settings from '@/pages/OrganizationRoleDetails/Settings'; import OrganizationTemplate from '@/pages/OrganizationTemplate'; import OrganizationPermissions from '@/pages/OrganizationTemplate/OrganizationPermissions'; import OrganizationRoles from '@/pages/OrganizationTemplate/OrganizationRoles'; @@ -25,5 +27,13 @@ export const organizationTemplate: RouteObject[] = [ { path: `organization-template/${OrganizationTemplateTabs.OrganizationRoles}/:id/*`, element: , + children: [ + { + index: true, + element: , + }, + { path: OrganizationRoleDetailsTabs.Permissions, element: }, + { path: OrganizationRoleDetailsTabs.General, element: }, + ], }, ]; diff --git a/packages/console/src/pages/ApiResourceDetails/ApiResourcePermissions/index.tsx b/packages/console/src/pages/ApiResourceDetails/ApiResourcePermissions/index.tsx index a586114cf..05033cab4 100644 --- a/packages/console/src/pages/ApiResourceDetails/ApiResourcePermissions/index.tsx +++ b/packages/console/src/pages/ApiResourceDetails/ApiResourcePermissions/index.tsx @@ -1,8 +1,9 @@ -import { isManagementApi, type Resource, type ScopeResponse } from '@logto/schemas'; +import { type ScopeResponse } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { useState } from 'react'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; +import { useOutletContext } from 'react-router-dom'; import useSWR from 'swr'; import PermissionsTable from '@/components/PermissionsTable'; @@ -12,17 +13,17 @@ import useApi from '@/hooks/use-api'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher'; import { buildUrl, formatSearchKeyword } from '@/utils/url'; +import { type ApiResourceDetailsOutletContext } from '../types'; + import CreatePermissionModal from './components/CreatePermissionModal'; const pageSize = defaultPageSize; -type Props = { - resource: Resource; -}; - -function ApiResourcePermissions({ resource }: Props) { - const { id: resourceId, indicator } = resource; - const isLogtoManagementApiResource = isManagementApi(indicator); +function ApiResourcePermissions() { + const { + resource: { id: resourceId }, + isLogtoManagementApiResource, + } = useOutletContext(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); diff --git a/packages/console/src/pages/ApiResourceDetails/ApiResourceSettings/index.tsx b/packages/console/src/pages/ApiResourceDetails/ApiResourceSettings/index.tsx index 7b2ff862f..48cc37de9 100644 --- a/packages/console/src/pages/ApiResourceDetails/ApiResourceSettings/index.tsx +++ b/packages/console/src/pages/ApiResourceDetails/ApiResourceSettings/index.tsx @@ -1,7 +1,8 @@ -import { isManagementApi, type Resource } from '@logto/schemas'; +import { type Resource } from '@logto/schemas'; import { useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { Trans, useTranslation } from 'react-i18next'; +import { useOutletContext } from 'react-router-dom'; import DetailsForm from '@/components/DetailsForm'; import FormCard from '@/components/FormCard'; @@ -14,14 +15,11 @@ import useApi from '@/hooks/use-api'; import useDocumentationUrl from '@/hooks/use-documentation-url'; import { trySubmitSafe } from '@/utils/form'; -type Props = { - resource: Resource; - isDeleting: boolean; - onResourceUpdated: (updatedData: Resource) => void; -}; +import { type ApiResourceDetailsOutletContext } from '../types'; -function ApiResourceSettings({ resource, isDeleting, onResourceUpdated }: Props) { - const isLogtoManagementApiResource = isManagementApi(resource.indicator); +function ApiResourceSettings() { + const { resource, isDeleting, isLogtoManagementApiResource, onResourceUpdated } = + useOutletContext(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { getDocumentationUrl } = useDocumentationUrl(); diff --git a/packages/console/src/pages/ApiResourceDetails/index.tsx b/packages/console/src/pages/ApiResourceDetails/index.tsx index f8dddb270..7635a3fa1 100644 --- a/packages/console/src/pages/ApiResourceDetails/index.tsx +++ b/packages/console/src/pages/ApiResourceDetails/index.tsx @@ -6,9 +6,7 @@ import classNames from 'classnames'; import { useEffect, useState } from 'react'; import { toast } from 'react-hot-toast'; import { Trans, useTranslation } from 'react-i18next'; -// FIXME: @yijun -// eslint-disable-next-line no-restricted-imports -import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom'; +import { Outlet, useLocation, useParams } from 'react-router-dom'; import useSWR from 'swr'; import ApiResourceDark from '@/assets/icons/api-resource-dark.svg'; @@ -30,12 +28,11 @@ import useDocumentationUrl from '@/hooks/use-documentation-url'; import useTenantPathname from '@/hooks/use-tenant-pathname'; import useTheme from '@/hooks/use-theme'; -import ApiResourcePermissions from './ApiResourcePermissions'; -import ApiResourceSettings from './ApiResourceSettings'; import GuideDrawer from './components/GuideDrawer'; import GuideModal from './components/GuideModal'; import ManagementApiNotice from './components/ManagementApiNotice'; import * as styles from './index.module.scss'; +import { type ApiResourceDetailsOutletContext } from './types'; const icons = { [Theme.Light]: { ApiIcon: ApiResource, ManagementApiIcon: ManagementApiResource }, @@ -180,25 +177,16 @@ function ApiResourceDetails() { {t('api_resource_details.general_tab')} - - } /> - } - /> - { - void mutate(updatedData); - }} - /> - } - /> - + )} diff --git a/packages/console/src/pages/ApiResourceDetails/types.ts b/packages/console/src/pages/ApiResourceDetails/types.ts new file mode 100644 index 000000000..455720bef --- /dev/null +++ b/packages/console/src/pages/ApiResourceDetails/types.ts @@ -0,0 +1,8 @@ +import type { Resource } from '@logto/schemas'; + +export type ApiResourceDetailsOutletContext = { + resource: Resource; + isDeleting: boolean; + isLogtoManagementApiResource: boolean; + onResourceUpdated: (resource: Resource) => void; +}; diff --git a/packages/console/src/pages/OrganizationRoleDetails/Permissions/index.tsx b/packages/console/src/pages/OrganizationRoleDetails/Permissions/index.tsx index 6b799c265..5dde3b42b 100644 --- a/packages/console/src/pages/OrganizationRoleDetails/Permissions/index.tsx +++ b/packages/console/src/pages/OrganizationRoleDetails/Permissions/index.tsx @@ -2,6 +2,7 @@ import { type Scope, type OrganizationScope } from '@logto/schemas'; import { useCallback, useMemo, useState } from 'react'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; +import { useOutletContext } from 'react-router-dom'; import Plus from '@/assets/icons/plus.svg'; import ActionsButton from '@/components/ActionsButton'; @@ -17,6 +18,8 @@ import Tag from '@/ds-components/Tag'; import useApi from '@/hooks/use-api'; import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher'; +import { type OrganizationRoleDetailsOutletContext } from '../types'; + import ResourceName from './ResourceName'; import * as styles from './index.module.scss'; import useOrganizationRoleScopes from './use-organization-role-scopes'; @@ -25,11 +28,11 @@ type OrganizationRoleScope = OrganizationScope | Scope; const isResourceScope = (scope: OrganizationRoleScope): scope is Scope => 'resourceId' in scope; -type Props = { - organizationRoleId: string; -}; +function Permissions() { + const { + organizationRole: { id: organizationRoleId }, + } = useOutletContext(); -function Permissions({ organizationRoleId }: Props) { const organizationRolePath = `api/organization-roles/${organizationRoleId}`; const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const api = useApi(); diff --git a/packages/console/src/pages/OrganizationRoleDetails/Settings/index.tsx b/packages/console/src/pages/OrganizationRoleDetails/Settings/index.tsx index 70615c58f..3872c33f5 100644 --- a/packages/console/src/pages/OrganizationRoleDetails/Settings/index.tsx +++ b/packages/console/src/pages/OrganizationRoleDetails/Settings/index.tsx @@ -2,6 +2,7 @@ import { type OrganizationRole } from '@logto/schemas'; import { useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; +import { useOutletContext } from 'react-router-dom'; import DetailsForm from '@/components/DetailsForm'; import FormCard from '@/components/FormCard'; @@ -13,12 +14,12 @@ import useApi from '@/hooks/use-api'; import useDocumentationUrl from '@/hooks/use-documentation-url'; import { trySubmitSafe } from '@/utils/form'; -type Props = { - data: OrganizationRole; - onUpdate: (updatedData: OrganizationRole) => void; -}; +import { type OrganizationRoleDetailsOutletContext } from '../types'; + +function Settings() { + const { organizationRole, isDeleting, onOrganizationRoleUpdated } = + useOutletContext(); -function Settings({ data, onUpdate }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { getDocumentationUrl } = useDocumentationUrl(); const { @@ -26,17 +27,17 @@ function Settings({ data, onUpdate }: Props) { handleSubmit, reset, formState: { errors, isDirty, isSubmitting }, - } = useForm({ defaultValues: data }); + } = useForm({ defaultValues: organizationRole }); const api = useApi(); const onSubmit = handleSubmit( trySubmitSafe(async (formData) => { const updatedData = await api - .patch(`api/organization-roles/${data.id}`, { json: formData }) + .patch(`api/organization-roles/${organizationRole.id}`, { json: formData }) .json(); reset(updatedData); - onUpdate(updatedData); + onOrganizationRoleUpdated(updatedData); toast.success(t('general.saved')); }) ); @@ -70,7 +71,9 @@ function Settings({ data, onUpdate }: Props) { /> - + ); } diff --git a/packages/console/src/pages/OrganizationRoleDetails/index.tsx b/packages/console/src/pages/OrganizationRoleDetails/index.tsx index d7d162520..b64fbf8ce 100644 --- a/packages/console/src/pages/OrganizationRoleDetails/index.tsx +++ b/packages/console/src/pages/OrganizationRoleDetails/index.tsx @@ -1,12 +1,10 @@ import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact'; import { type OrganizationRole } from '@logto/schemas'; import classNames from 'classnames'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; -// FIXME: @yijun -// eslint-disable-next-line no-restricted-imports -import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom'; +import { Outlet, useLocation, useParams } from 'react-router-dom'; import useSWR, { useSWRConfig } from 'swr'; import Delete from '@/assets/icons/delete.svg'; @@ -22,9 +20,8 @@ import TabNav, { TabNavItem } from '@/ds-components/TabNav'; import useApi, { type RequestError } from '@/hooks/use-api'; import useTenantPathname from '@/hooks/use-tenant-pathname'; -import Permissions from './Permissions'; -import Settings from './Settings'; import * as styles from './index.module.scss'; +import { type OrganizationRoleDetailsOutletContext } from './types'; const orgRolesPath = `/organization-template/${OrganizationTemplateTabs.OrganizationRoles}`; @@ -44,6 +41,11 @@ function OrganizationRoleDetails() { const [isDeletionAlertOpen, setIsDeletionAlertOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); + // Close deletion alert when navigating to another page + useEffect(() => { + setIsDeletionAlertOpen(false); + }, [pathname]); + const handleDelete = async () => { if (!data) { return; @@ -110,20 +112,15 @@ function OrganizationRoleDetails() { - - } - /> - } - /> - } - /> - + )} diff --git a/packages/console/src/pages/OrganizationRoleDetails/types.ts b/packages/console/src/pages/OrganizationRoleDetails/types.ts new file mode 100644 index 000000000..d77bbe0ce --- /dev/null +++ b/packages/console/src/pages/OrganizationRoleDetails/types.ts @@ -0,0 +1,7 @@ +import { type OrganizationRole } from '@logto/schemas'; + +export type OrganizationRoleDetailsOutletContext = { + organizationRole: OrganizationRole; + isDeleting: boolean; + onOrganizationRoleUpdated: (organizationRole: OrganizationRole) => void; +};