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:
chauhan_s
2026-04-06 23:38:36 +05:30
committed by GitHub
parent 5806ad8a3a
commit e3391c0577
11 changed files with 301 additions and 20 deletions
@@ -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',
}
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
/**
* Use this for over workbench navigate, for navigate in workbench, use `WorkbenchService`.
@@ -213,18 +265,15 @@ export function useNavigateHelper() {
const jumpToWorkspaceSettings = useCallback(
(
workspaceId: string,
tab?: SettingTab,
options?: WorkspaceSettingsRouteOptions | SettingTab,
logic: RouteLogic = RouteLogic.PUSH
) => {
const searchParams = new URLSearchParams();
if (tab) {
searchParams.set('tab', tab);
}
const resolvedOptions =
typeof options === 'string' ? { tab: options } : options;
return navigate(
`/workspace/${workspaceId}/settings?${searchParams.toString()}`,
{
replace: logic === RouteLogic.REPLACE,
}
buildWorkspaceSettingsPath(workspaceId, resolvedOptions),
{ replace: logic === RouteLogic.REPLACE }
);
},
[navigate]