mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 05:05:52 +08:00
feat: redirect account click & OAuth to Calendar settings (#14693)
### PR Description * clicking a linked calendar account now switches settings to Workspace Integrations and opens the Calendar settings directly * calendar OAuth returns now land on Workspace Integrations with the Calendar settings opened instead of the homepage * Improves UX by reducing friction when managing calendar integrations https://www.loom.com/share/49fa5c448ce049659877beb42d7bd81a <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Calendar integration settings can now be opened automatically (including from OAuth redirects) and workspace settings support a scroll-to-anchor. * Integration account rows are now clickable for quick access to settings. * **Improvements** * Enhanced visual feedback with interactive hover and focus states for integration controls. * **Tests** * Added tests covering the OAuth redirect behavior and workspace settings scroll/open handling. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DarkSky <darksky2048@gmail.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment happy-dom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildWorkspaceSettingsPath,
|
||||||
|
buildWorkspaceSettingsRedirectUri,
|
||||||
|
} from '../use-navigate-helper';
|
||||||
|
|
||||||
|
describe('use-navigate-helper utilities', () => {
|
||||||
|
test('buildWorkspaceSettingsPath includes tab and scroll anchor', () => {
|
||||||
|
expect(
|
||||||
|
buildWorkspaceSettingsPath('workspace-1', {
|
||||||
|
tab: 'workspace:integrations',
|
||||||
|
scrollAnchor: 'integration-calendar',
|
||||||
|
})
|
||||||
|
).toBe(
|
||||||
|
'/workspace/workspace-1/settings?tab=workspace%3Aintegrations&scrollAnchor=integration-calendar'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildWorkspaceSettingsRedirectUri builds a settings redirect from a workspace page', () => {
|
||||||
|
expect(
|
||||||
|
buildWorkspaceSettingsRedirectUri(
|
||||||
|
'https://app.affine.pro/workspace/workspace-1/all',
|
||||||
|
{
|
||||||
|
tab: 'workspace:integrations',
|
||||||
|
scrollAnchor: 'integration-calendar',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toBe(
|
||||||
|
'https://app.affine.pro/workspace/workspace-1/settings?tab=workspace%3Aintegrations&scrollAnchor=integration-calendar'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildWorkspaceSettingsRedirectUri preserves app subpaths before the workspace route', () => {
|
||||||
|
expect(
|
||||||
|
buildWorkspaceSettingsRedirectUri(
|
||||||
|
'https://app.affine.pro/app/workspace/workspace-1/collection',
|
||||||
|
{
|
||||||
|
tab: 'workspace:integrations',
|
||||||
|
scrollAnchor: 'integration-calendar',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toBe(
|
||||||
|
'https://app.affine.pro/app/workspace/workspace-1/settings?tab=workspace%3Aintegrations&scrollAnchor=integration-calendar'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildWorkspaceSettingsRedirectUri falls back to the current url when no workspace route is present', () => {
|
||||||
|
expect(
|
||||||
|
buildWorkspaceSettingsRedirectUri('https://app.affine.pro/sign-in', {
|
||||||
|
tab: 'workspace:integrations',
|
||||||
|
})
|
||||||
|
).toBe('https://app.affine.pro/sign-in');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,6 +17,58 @@ export enum RouteLogic {
|
|||||||
PUSH = 'push',
|
PUSH = 'push',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkspaceSettingsRouteOptions = {
|
||||||
|
tab?: SettingTab;
|
||||||
|
scrollAnchor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildWorkspaceSettingsPath(
|
||||||
|
workspaceId: string,
|
||||||
|
options?: WorkspaceSettingsRouteOptions
|
||||||
|
) {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (options?.tab) {
|
||||||
|
searchParams.set('tab', options.tab);
|
||||||
|
}
|
||||||
|
if (options?.scrollAnchor) {
|
||||||
|
searchParams.set('scrollAnchor', options.scrollAnchor);
|
||||||
|
}
|
||||||
|
const query = searchParams.toString();
|
||||||
|
return `/workspace/${workspaceId}/settings${query ? `?${query}` : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWorkspaceSettingsRedirectUri(
|
||||||
|
currentHref: string,
|
||||||
|
options?: WorkspaceSettingsRouteOptions
|
||||||
|
): string {
|
||||||
|
let currentUrl: URL;
|
||||||
|
try {
|
||||||
|
currentUrl = new URL(currentHref);
|
||||||
|
} catch {
|
||||||
|
return currentHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathSegments = currentUrl.pathname.split('/').filter(Boolean);
|
||||||
|
const workspaceSegmentIndex = pathSegments.indexOf('workspace');
|
||||||
|
const workspaceId = pathSegments[workspaceSegmentIndex + 1];
|
||||||
|
|
||||||
|
if (workspaceSegmentIndex === -1 || !workspaceId) {
|
||||||
|
return currentHref;
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = pathSegments.slice(0, workspaceSegmentIndex).join('/');
|
||||||
|
const redirectUrl = new URL(
|
||||||
|
buildWorkspaceSettingsPath(workspaceId, options),
|
||||||
|
currentUrl.origin
|
||||||
|
);
|
||||||
|
|
||||||
|
if (basePath) {
|
||||||
|
redirectUrl.pathname = `/${basePath}${redirectUrl.pathname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirectUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(@eyhn): add a name -> path helper in the results
|
// TODO(@eyhn): add a name -> path helper in the results
|
||||||
/**
|
/**
|
||||||
* Use this for over workbench navigate, for navigate in workbench, use `WorkbenchService`.
|
* Use this for over workbench navigate, for navigate in workbench, use `WorkbenchService`.
|
||||||
@@ -213,18 +265,15 @@ export function useNavigateHelper() {
|
|||||||
const jumpToWorkspaceSettings = useCallback(
|
const jumpToWorkspaceSettings = useCallback(
|
||||||
(
|
(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
tab?: SettingTab,
|
options?: WorkspaceSettingsRouteOptions | SettingTab,
|
||||||
logic: RouteLogic = RouteLogic.PUSH
|
logic: RouteLogic = RouteLogic.PUSH
|
||||||
) => {
|
) => {
|
||||||
const searchParams = new URLSearchParams();
|
const resolvedOptions =
|
||||||
if (tab) {
|
typeof options === 'string' ? { tab: options } : options;
|
||||||
searchParams.set('tab', tab);
|
|
||||||
}
|
|
||||||
return navigate(
|
return navigate(
|
||||||
`/workspace/${workspaceId}/settings?${searchParams.toString()}`,
|
buildWorkspaceSettingsPath(workspaceId, resolvedOptions),
|
||||||
{
|
{ replace: logic === RouteLogic.REPLACE }
|
||||||
replace: logic === RouteLogic.REPLACE,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[navigate]
|
[navigate]
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export const AccountSetting = ({
|
|||||||
{serverFeatures?.copilot && (
|
{serverFeatures?.copilot && (
|
||||||
<AIUsagePanel onChangeSettingState={onChangeSettingState} />
|
<AIUsagePanel onChangeSettingState={onChangeSettingState} />
|
||||||
)}
|
)}
|
||||||
<IntegrationsPanel />
|
<IntegrationsPanel onChangeSettingState={onChangeSettingState} />
|
||||||
<SettingRow
|
<SettingRow
|
||||||
name={t[`Sign out`]()}
|
name={t[`Sign out`]()}
|
||||||
desc={t['com.affine.setting.sign.out.message']()}
|
desc={t['com.affine.setting.sign.out.message']()}
|
||||||
|
|||||||
+14
@@ -46,6 +46,20 @@ export const accountRow = style({
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
background: cssVarV2.layer.background.primary,
|
background: cssVarV2.layer.background.primary,
|
||||||
|
transition: 'background-color 0.15s ease, border-color 0.15s ease',
|
||||||
|
selectors: {
|
||||||
|
'&[data-interactive="true"]': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
'&[data-interactive="true"]:hover': {
|
||||||
|
background: cssVarV2.layer.background.hoverOverlay,
|
||||||
|
borderColor: cssVarV2.layer.insideBorder.blackBorder,
|
||||||
|
},
|
||||||
|
'&[data-interactive="true"]:focus-visible': {
|
||||||
|
outline: `2px solid ${cssVarV2.layer.insideBorder.primaryBorder}`,
|
||||||
|
outlineOffset: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const accountInfo = style({
|
export const accountInfo = style({
|
||||||
|
|||||||
+57
-5
@@ -7,6 +7,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
notify,
|
notify,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
|
import { buildWorkspaceSettingsRedirectUri } from '@affine/core/components/hooks/use-navigate-helper';
|
||||||
import {
|
import {
|
||||||
useQuery,
|
useQuery,
|
||||||
type UseQueryConfig,
|
type UseQueryConfig,
|
||||||
@@ -30,6 +31,7 @@ import { GoogleIcon, LinkIcon, TodayIcon } from '@blocksuite/icons/rc';
|
|||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
type FormEvent,
|
type FormEvent,
|
||||||
|
type KeyboardEvent,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -37,7 +39,10 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { WorkspaceService } from '../../../../modules/workspace';
|
||||||
import { CollapsibleWrapper } from '../layout';
|
import { CollapsibleWrapper } from '../layout';
|
||||||
|
import { CALENDAR_INTEGRATION_SCROLL_ANCHOR } from '../navigation-constants';
|
||||||
|
import type { SettingState } from '../types';
|
||||||
import * as styles from './integrations-panel.css';
|
import * as styles from './integrations-panel.css';
|
||||||
|
|
||||||
type CalendarAccount = NonNullable<
|
type CalendarAccount = NonNullable<
|
||||||
@@ -323,16 +328,22 @@ const CalDAVLinkDialog = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IntegrationsPanel = () => {
|
export const IntegrationsPanel = ({
|
||||||
|
onChangeSettingState,
|
||||||
|
}: {
|
||||||
|
onChangeSettingState?: (settingState: SettingState) => void;
|
||||||
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const gqlService = useService(GraphQLService);
|
const gqlService = useService(GraphQLService);
|
||||||
const urlService = useService(UrlService);
|
const urlService = useService(UrlService);
|
||||||
|
const workspaceService = useService(WorkspaceService);
|
||||||
const [linking, setLinking] = useState(false);
|
const [linking, setLinking] = useState(false);
|
||||||
const [unlinkingAccountId, setUnlinkingAccountId] = useState<string | null>(
|
const [unlinkingAccountId, setUnlinkingAccountId] = useState<string | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [openedExternalWindow, setOpenedExternalWindow] = useState(false);
|
const [openedExternalWindow, setOpenedExternalWindow] = useState(false);
|
||||||
const [caldavDialogOpen, setCaldavDialogOpen] = useState(false);
|
const [caldavDialogOpen, setCaldavDialogOpen] = useState(false);
|
||||||
|
const canOpenCalendarSetting = workspaceService.workspace.flavour !== 'local';
|
||||||
const makeConfig: <Query extends GraphQLQuery>(
|
const makeConfig: <Query extends GraphQLQuery>(
|
||||||
title: string
|
title: string
|
||||||
) => UseQueryConfig<Query> = useCallback(
|
) => UseQueryConfig<Query> = useCallback(
|
||||||
@@ -394,6 +405,27 @@ export const IntegrationsPanel = () => {
|
|||||||
});
|
});
|
||||||
}, [providers]);
|
}, [providers]);
|
||||||
|
|
||||||
|
const handleOpenCalendarSetting = useCallback(() => {
|
||||||
|
if (!canOpenCalendarSetting) return;
|
||||||
|
|
||||||
|
onChangeSettingState?.({
|
||||||
|
activeTab: 'workspace:integrations',
|
||||||
|
scrollAnchor: CALENDAR_INTEGRATION_SCROLL_ANCHOR,
|
||||||
|
});
|
||||||
|
}, [canOpenCalendarSetting, onChangeSettingState]);
|
||||||
|
|
||||||
|
const handleAccountRowKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (!canOpenCalendarSetting) return;
|
||||||
|
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleOpenCalendarSetting();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[canOpenCalendarSetting, handleOpenCalendarSetting]
|
||||||
|
);
|
||||||
|
|
||||||
const handleLink = useCallback(
|
const handleLink = useCallback(
|
||||||
async (provider: CalendarProviderType) => {
|
async (provider: CalendarProviderType) => {
|
||||||
if (provider === CalendarProviderType.CalDAV) {
|
if (provider === CalendarProviderType.CalDAV) {
|
||||||
@@ -408,7 +440,12 @@ export const IntegrationsPanel = () => {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
provider,
|
provider,
|
||||||
redirectUri: window.location.href,
|
redirectUri: canOpenCalendarSetting
|
||||||
|
? buildWorkspaceSettingsRedirectUri(window.location.href, {
|
||||||
|
tab: 'workspace:integrations',
|
||||||
|
scrollAnchor: CALENDAR_INTEGRATION_SCROLL_ANCHOR,
|
||||||
|
})
|
||||||
|
: window.location.href,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -423,7 +460,7 @@ export const IntegrationsPanel = () => {
|
|||||||
setLinking(false);
|
setLinking(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[gqlService, t, urlService]
|
[canOpenCalendarSetting, gqlService, t, urlService]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUnlink = useCallback(
|
const handleUnlink = useCallback(
|
||||||
@@ -535,7 +572,19 @@ export const IntegrationsPanel = () => {
|
|||||||
]();
|
]();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={account.id} className={styles.accountRow}>
|
<div
|
||||||
|
key={account.id}
|
||||||
|
className={styles.accountRow}
|
||||||
|
data-interactive={canOpenCalendarSetting}
|
||||||
|
onClick={
|
||||||
|
canOpenCalendarSetting
|
||||||
|
? handleOpenCalendarSetting
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onKeyDown={handleAccountRowKeyDown}
|
||||||
|
role={canOpenCalendarSetting ? 'button' : undefined}
|
||||||
|
tabIndex={canOpenCalendarSetting ? 0 : undefined}
|
||||||
|
>
|
||||||
<div className={styles.accountInfo}>
|
<div className={styles.accountInfo}>
|
||||||
<div className={styles.accountIcon}>
|
<div className={styles.accountIcon}>
|
||||||
{meta?.icon ?? <LinkIcon />}
|
{meta?.icon ?? <LinkIcon />}
|
||||||
@@ -562,7 +611,10 @@ export const IntegrationsPanel = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="error"
|
variant="error"
|
||||||
disabled={unlinkingAccountId === account.id}
|
disabled={unlinkingAccountId === account.id}
|
||||||
onClick={() => void handleUnlink(account.id)}
|
onClick={event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
handleUnlink(account.id).catch(() => undefined);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t['com.affine.integration.calendar.account.unlink']()}
|
{t['com.affine.integration.calendar.account.unlink']()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ const SettingModalInner = ({
|
|||||||
) : isWorkspaceSetting(settingState.activeTab) ? (
|
) : isWorkspaceSetting(settingState.activeTab) ? (
|
||||||
<WorkspaceSetting
|
<WorkspaceSetting
|
||||||
activeTab={settingState.activeTab}
|
activeTab={settingState.activeTab}
|
||||||
|
scrollAnchor={settingState.scrollAnchor}
|
||||||
onCloseSetting={onCloseSetting}
|
onCloseSetting={onCloseSetting}
|
||||||
onChangeSettingState={setSettingState}
|
onChangeSettingState={setSettingState}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const CALENDAR_INTEGRATION_SCROLL_ANCHOR = 'integration-calendar';
|
||||||
@@ -28,10 +28,12 @@ import { WorkspaceSettingStorage } from './storage';
|
|||||||
|
|
||||||
export const WorkspaceSetting = ({
|
export const WorkspaceSetting = ({
|
||||||
activeTab,
|
activeTab,
|
||||||
|
scrollAnchor,
|
||||||
onCloseSetting,
|
onCloseSetting,
|
||||||
onChangeSettingState,
|
onChangeSettingState,
|
||||||
}: {
|
}: {
|
||||||
activeTab: SettingTab;
|
activeTab: SettingTab;
|
||||||
|
scrollAnchor?: string;
|
||||||
onCloseSetting: () => void;
|
onCloseSetting: () => void;
|
||||||
onChangeSettingState: (settingState: SettingState) => void;
|
onChangeSettingState: (settingState: SettingState) => void;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -54,7 +56,7 @@ export const WorkspaceSetting = ({
|
|||||||
case 'workspace:license':
|
case 'workspace:license':
|
||||||
return <WorkspaceSettingLicense onCloseSetting={onCloseSetting} />;
|
return <WorkspaceSettingLicense onCloseSetting={onCloseSetting} />;
|
||||||
case 'workspace:integrations':
|
case 'workspace:integrations':
|
||||||
return <IntegrationSetting />;
|
return <IntegrationSetting scrollAnchor={scrollAnchor} />;
|
||||||
case 'workspace:embedding':
|
case 'workspace:embedding':
|
||||||
return <EmbeddingSettings />;
|
return <EmbeddingSettings />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
+27
-3
@@ -2,8 +2,15 @@ import { SettingHeader } from '@affine/component/setting-components';
|
|||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { CALENDAR_INTEGRATION_SCROLL_ANCHOR } from '../../navigation-constants';
|
||||||
import { SubPageProvider, useSubPageIsland } from '../../sub-page';
|
import { SubPageProvider, useSubPageIsland } from '../../sub-page';
|
||||||
import {
|
import {
|
||||||
IntegrationCard,
|
IntegrationCard,
|
||||||
@@ -14,17 +21,34 @@ import { getAllowedIntegrationList } from './constants';
|
|||||||
import { type IntegrationItem } from './constants';
|
import { type IntegrationItem } from './constants';
|
||||||
import { list } from './index.css';
|
import { list } from './index.css';
|
||||||
|
|
||||||
export const IntegrationSetting = () => {
|
export const IntegrationSetting = ({
|
||||||
|
scrollAnchor,
|
||||||
|
}: {
|
||||||
|
scrollAnchor?: string;
|
||||||
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [opened, setOpened] = useState<string | null>(null);
|
const [opened, setOpened] = useState<string | null>(null);
|
||||||
const workspaceService = useService(WorkspaceService);
|
const workspaceService = useService(WorkspaceService);
|
||||||
const isCloudWorkspace = workspaceService.workspace.flavour !== 'local';
|
const isCloudWorkspace = workspaceService.workspace.flavour !== 'local';
|
||||||
console.log('isCloudWorkspace', isCloudWorkspace);
|
|
||||||
|
|
||||||
const integrationList = useMemo(
|
const integrationList = useMemo(
|
||||||
() => getAllowedIntegrationList(isCloudWorkspace),
|
() => getAllowedIntegrationList(isCloudWorkspace),
|
||||||
[isCloudWorkspace]
|
[isCloudWorkspace]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollAnchor !== CALENDAR_INTEGRATION_SCROLL_ANCHOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCalendarSetting = integrationList.some(
|
||||||
|
item => item.id === 'calendar' && 'setting' in item
|
||||||
|
);
|
||||||
|
if (hasCalendarSetting) {
|
||||||
|
setOpened('calendar');
|
||||||
|
}
|
||||||
|
}, [integrationList, scrollAnchor]);
|
||||||
|
|
||||||
const handleCardClick = useCallback((card: IntegrationItem) => {
|
const handleCardClick = useCallback((card: IntegrationItem) => {
|
||||||
if ('setting' in card && card.setting) {
|
if ('setting' in card && card.setting) {
|
||||||
setOpened(card.id);
|
setOpened(card.id);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment happy-dom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import type * as ReactRouterDom from 'react-router-dom';
|
||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
const openAll = vi.hoisted(() => vi.fn());
|
||||||
|
const openDialog = vi.hoisted(() => vi.fn());
|
||||||
|
const searchParamsState = vi.hoisted(() => ({
|
||||||
|
value: new URLSearchParams(),
|
||||||
|
}));
|
||||||
|
const WorkspaceDialogServiceToken = vi.hoisted(
|
||||||
|
() => class WorkspaceDialogService {}
|
||||||
|
);
|
||||||
|
const WorkbenchServiceToken = vi.hoisted(() => class WorkbenchService {});
|
||||||
|
|
||||||
|
vi.mock('@affine/core/modules/dialogs', () => ({
|
||||||
|
WorkspaceDialogService: WorkspaceDialogServiceToken,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@affine/core/modules/workbench', () => ({
|
||||||
|
WorkbenchService: WorkbenchServiceToken,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@toeverything/infra', () => ({
|
||||||
|
useService: (token: unknown) => {
|
||||||
|
if (token === WorkbenchServiceToken) {
|
||||||
|
return {
|
||||||
|
workbench: {
|
||||||
|
openAll,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (token === WorkspaceDialogServiceToken) {
|
||||||
|
return {
|
||||||
|
open: openDialog,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('react-router-dom', async () => {
|
||||||
|
const actual =
|
||||||
|
await vi.importActual<typeof ReactRouterDom>('react-router-dom');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useSearchParams: () => [searchParamsState.value],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { Component } from './index';
|
||||||
|
|
||||||
|
describe('workspace settings page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
openAll.mockReset();
|
||||||
|
openDialog.mockReset();
|
||||||
|
searchParamsState.value = new URLSearchParams();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes tab and scrollAnchor through to the settings dialog', () => {
|
||||||
|
searchParamsState.value = new URLSearchParams({
|
||||||
|
tab: 'workspace:integrations',
|
||||||
|
scrollAnchor: 'integration-calendar',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<Component />);
|
||||||
|
|
||||||
|
expect(openAll).toHaveBeenCalled();
|
||||||
|
expect(openDialog).toHaveBeenCalledWith('setting', {
|
||||||
|
activeTab: 'workspace:integrations',
|
||||||
|
scrollAnchor: 'integration-calendar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@ export const Component = () => {
|
|||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const tab = searchParams.get('tab') ?? undefined;
|
const tab = searchParams.get('tab') ?? undefined;
|
||||||
|
const scrollAnchor = searchParams.get('scrollAnchor') ?? undefined;
|
||||||
|
|
||||||
const isOpened = useRef(false);
|
const isOpened = useRef(false);
|
||||||
|
|
||||||
@@ -23,7 +24,8 @@ export const Component = () => {
|
|||||||
workbench.openAll();
|
workbench.openAll();
|
||||||
workspaceDialogService.open('setting', {
|
workspaceDialogService.open('setting', {
|
||||||
activeTab: tab as SettingTab,
|
activeTab: tab as SettingTab,
|
||||||
|
scrollAnchor,
|
||||||
});
|
});
|
||||||
}, [tab, workbench, workspaceDialogService]);
|
}, [scrollAnchor, tab, workbench, workspaceDialogService]);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user