mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
refactor(core): new back&forward button base on workbench (#6012)
# feature: ## In Browser: - hidden back&forward button in sidebar. - back and forward is equal with `window.history.back()` `window.history.forward()` ## In Desktop: - Back and forward can be controlled through the sidebar, cmdk, and shortcut keys. - back and forward act on the currently **active** view. - buttons change disable&enable style based on current active view history # Refactor: Move app-sidebar and app-container from @affine/component to @affine/core
This commit is contained in:
@@ -182,7 +182,7 @@ export class LiveData<T = unknown> implements InteropObservable<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribe(
|
subscribe(
|
||||||
observer: Partial<Observer<T>> | ((value: T) => void) | undefined
|
observer?: Partial<Observer<T>> | ((value: T) => void) | undefined
|
||||||
): Subscription {
|
): Subscription {
|
||||||
this.ops.next('watch');
|
this.ops.next('watch');
|
||||||
const subscription = this.raw.subscribe(observer);
|
const subscription = this.raw.subscribe(observer);
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|
||||||
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { IconButton } from '../../../ui/button';
|
|
||||||
import { Tooltip } from '../../../ui/tooltip';
|
|
||||||
import type { History } from '..';
|
|
||||||
import {
|
|
||||||
navHeaderButton,
|
|
||||||
navHeaderNavigationButtons,
|
|
||||||
navHeaderStyle,
|
|
||||||
} from '../index.css';
|
|
||||||
import { appSidebarOpenAtom } from '../index.jotai';
|
|
||||||
import { SidebarSwitch } from './sidebar-switch';
|
|
||||||
|
|
||||||
export type SidebarHeaderProps = {
|
|
||||||
router?: {
|
|
||||||
back: () => unknown;
|
|
||||||
forward: () => unknown;
|
|
||||||
history: History;
|
|
||||||
};
|
|
||||||
generalShortcutsInfo?: {
|
|
||||||
shortcuts: {
|
|
||||||
[title: string]: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SidebarHeader = (props: SidebarHeaderProps) => {
|
|
||||||
const open = useAtomValue(appSidebarOpenAtom);
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
|
|
||||||
const shortcuts = props.generalShortcutsInfo?.shortcuts;
|
|
||||||
const shortcutsObject = useMemo(() => {
|
|
||||||
const goBack = t['com.affine.keyboardShortcuts.goBack']();
|
|
||||||
const goBackShortcut = shortcuts?.[goBack];
|
|
||||||
|
|
||||||
const goForward = t['com.affine.keyboardShortcuts.goForward']();
|
|
||||||
const goForwardShortcut = shortcuts?.[goForward];
|
|
||||||
return {
|
|
||||||
goBack,
|
|
||||||
goBackShortcut,
|
|
||||||
goForward,
|
|
||||||
goForwardShortcut,
|
|
||||||
};
|
|
||||||
}, [shortcuts, t]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={navHeaderStyle}
|
|
||||||
data-open={open}
|
|
||||||
data-is-macos-electron={environment.isDesktop && environment.isMacOs}
|
|
||||||
>
|
|
||||||
<SidebarSwitch show={open} />
|
|
||||||
<div className={navHeaderNavigationButtons}>
|
|
||||||
<Tooltip
|
|
||||||
content={`${shortcutsObject.goBack} ${shortcutsObject.goBackShortcut}`}
|
|
||||||
side="bottom"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
className={navHeaderButton}
|
|
||||||
data-testid="app-sidebar-arrow-button-back"
|
|
||||||
disabled={props.router?.history.current === 0}
|
|
||||||
onClick={() => {
|
|
||||||
props.router?.back();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowLeftSmallIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
content={`${shortcutsObject.goForward} ${shortcutsObject.goForwardShortcut}`}
|
|
||||||
side="bottom"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
className={navHeaderButton}
|
|
||||||
data-testid="app-sidebar-arrow-button-forward"
|
|
||||||
disabled={
|
|
||||||
props.router
|
|
||||||
? (props.router.history.stack.length > 0 &&
|
|
||||||
props.router.history.current ===
|
|
||||||
props.router.history.stack.length - 1) ||
|
|
||||||
props.router.history.stack.length === 0
|
|
||||||
: true
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
props.router?.forward();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowRightSmallIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export * from './sidebar-switch';
|
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './scrollable';
|
export * from './scrollable';
|
||||||
export * from './scrollbar';
|
export * from './scrollbar';
|
||||||
|
export * from './use-has-scroll-top';
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import * as ScrollArea from '@radix-ui/react-scroll-area';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { type PropsWithChildren, useRef } from 'react';
|
import { type PropsWithChildren, useRef } from 'react';
|
||||||
|
|
||||||
import { useHasScrollTop } from '../../components/app-sidebar/sidebar-containers/use-has-scroll-top';
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
|
import { useHasScrollTop } from './use-has-scroll-top';
|
||||||
|
|
||||||
export type ScrollableContainerProps = {
|
export type ScrollableContainerProps = {
|
||||||
showScrollTopBorder?: boolean;
|
showScrollTopBorder?: boolean;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import '@affine/component/theme/theme.css';
|
|||||||
import { AffineContext } from '@affine/component/context';
|
import { AffineContext } from '@affine/component/context';
|
||||||
import { GlobalLoading } from '@affine/component/global-loading';
|
import { GlobalLoading } from '@affine/component/global-loading';
|
||||||
import { NotificationCenter } from '@affine/component/notification-center';
|
import { NotificationCenter } from '@affine/component/notification-center';
|
||||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
|
||||||
import { createI18n, setUpLanguage } from '@affine/i18n';
|
import { createI18n, setUpLanguage } from '@affine/i18n';
|
||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
@@ -13,6 +12,7 @@ import type { PropsWithChildren, ReactElement } from 'react';
|
|||||||
import { lazy, memo, Suspense } from 'react';
|
import { lazy, memo, Suspense } from 'react';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { WorkspaceFallback } from './components/workspace';
|
||||||
import { GlobalScopeProvider } from './modules/infra-web/global-scope';
|
import { GlobalScopeProvider } from './modules/infra-web/global-scope';
|
||||||
import { CloudSessionProvider } from './providers/session-provider';
|
import { CloudSessionProvider } from './providers/session-provider';
|
||||||
import { router } from './router';
|
import { router } from './router';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// these atoms cannot be moved to @affine/jotai since they use atoms from @affine/component
|
// these atoms cannot be moved to @affine/jotai since they use atoms from @affine/component
|
||||||
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
|
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
|
||||||
|
import { appSidebarOpenAtom } from '../components/app-sidebar';
|
||||||
|
|
||||||
export type Guide = {
|
export type Guide = {
|
||||||
// should show quick search tips
|
// should show quick search tips
|
||||||
quickSearchTips: boolean;
|
quickSearchTips: boolean;
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
import { useAtom } from 'jotai';
|
|
||||||
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { router } from '../router';
|
|
||||||
|
|
||||||
export type History = {
|
|
||||||
stack: string[];
|
|
||||||
current: number;
|
|
||||||
skip: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MAX_HISTORY = 50;
|
|
||||||
|
|
||||||
const historyBaseAtom = atomWithStorage<History>(
|
|
||||||
'router-history',
|
|
||||||
{
|
|
||||||
stack: [],
|
|
||||||
current: 0,
|
|
||||||
skip: false,
|
|
||||||
},
|
|
||||||
createJSONStorage(() => sessionStorage)
|
|
||||||
);
|
|
||||||
|
|
||||||
historyBaseAtom.onMount = set => {
|
|
||||||
const unsubscribe = router.subscribe(state => {
|
|
||||||
set(prev => {
|
|
||||||
const url = state.location.pathname;
|
|
||||||
|
|
||||||
// if stack top is the same as current, skip
|
|
||||||
if (prev.stack[prev.current] === url) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prev.skip) {
|
|
||||||
return {
|
|
||||||
stack: [...prev.stack],
|
|
||||||
current: prev.current,
|
|
||||||
skip: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
if (prev.current < prev.stack.length - 1) {
|
|
||||||
const newStack = prev.stack.slice(0, prev.current);
|
|
||||||
newStack.push(url);
|
|
||||||
if (newStack.length > MAX_HISTORY) {
|
|
||||||
newStack.shift();
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
stack: newStack,
|
|
||||||
current: newStack.length - 1,
|
|
||||||
skip: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const newStack = [...prev.stack, url];
|
|
||||||
if (newStack.length > MAX_HISTORY) {
|
|
||||||
newStack.shift();
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
stack: newStack,
|
|
||||||
current: newStack.length - 1,
|
|
||||||
skip: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useHistoryAtom() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [base, setBase] = useAtom(historyBaseAtom);
|
|
||||||
return [
|
|
||||||
base,
|
|
||||||
useCallback(
|
|
||||||
(forward: boolean) => {
|
|
||||||
setBase(prev => {
|
|
||||||
if (forward) {
|
|
||||||
const target = Math.min(prev.stack.length - 1, prev.current + 1);
|
|
||||||
const url = prev.stack[target];
|
|
||||||
navigate(url);
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
current: target,
|
|
||||||
skip: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const target = Math.max(0, prev.current - 1);
|
|
||||||
const url = prev.stack[target];
|
|
||||||
navigate(url);
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
current: target,
|
|
||||||
skip: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[setBase, navigate]
|
|
||||||
),
|
|
||||||
] as const;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
|
|
||||||
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { SidebarIcon } from '@blocksuite/icons';
|
import { SidebarIcon } from '@blocksuite/icons';
|
||||||
import { registerAffineCommand } from '@toeverything/infra/command';
|
import { registerAffineCommand } from '@toeverything/infra/command';
|
||||||
import type { createStore } from 'jotai';
|
import type { createStore } from 'jotai';
|
||||||
|
|
||||||
|
import { appSidebarOpenAtom } from '../components/app-sidebar';
|
||||||
|
|
||||||
export function registerAffineLayoutCommands({
|
export function registerAffineLayoutCommands({
|
||||||
t,
|
t,
|
||||||
store,
|
store,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
||||||
import {
|
import {
|
||||||
AppContainer as AppContainerWithoutSettings,
|
AppContainer as AppContainerWithoutSettings,
|
||||||
type WorkspaceRootProps,
|
type WorkspaceRootProps,
|
||||||
} from '@affine/component/workspace';
|
} from '../workspace';
|
||||||
|
|
||||||
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
|
||||||
|
|
||||||
export const AppContainer = (props: WorkspaceRootProps) => {
|
export const AppContainer = (props: WorkspaceRootProps) => {
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ToolContainer } from '@affine/component/workspace';
|
|
||||||
|
|
||||||
import { HelpIsland } from '../../pure/help-island';
|
import { HelpIsland } from '../../pure/help-island';
|
||||||
|
import { ToolContainer } from '../../workspace';
|
||||||
|
|
||||||
export const HubIsland = () => {
|
export const HubIsland = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,10 +1,10 @@
|
|||||||
|
import { Tooltip } from '@affine/component';
|
||||||
import { Unreachable } from '@affine/env/constant';
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons';
|
import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { Tooltip } from '../../../ui/tooltip';
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
|
|
||||||
export interface AddPageButtonProps {
|
export interface AddPageButtonProps {
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { Skeleton } from '@affine/component';
|
||||||
|
import { ResizePanel } from '@affine/component/resize-panel';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Skeleton } from '../../ui/skeleton';
|
|
||||||
import { ResizePanel } from '../resize-panel';
|
|
||||||
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
||||||
import {
|
import {
|
||||||
floatingMaxWidth,
|
floatingMaxWidth,
|
||||||
@@ -21,14 +21,11 @@ import {
|
|||||||
appSidebarResizingAtom,
|
appSidebarResizingAtom,
|
||||||
appSidebarWidthAtom,
|
appSidebarWidthAtom,
|
||||||
} from './index.jotai';
|
} from './index.jotai';
|
||||||
import type { SidebarHeaderProps } from './sidebar-header';
|
|
||||||
import { SidebarHeader } from './sidebar-header';
|
import { SidebarHeader } from './sidebar-header';
|
||||||
|
|
||||||
export type AppSidebarProps = PropsWithChildren<
|
export type AppSidebarProps = PropsWithChildren<{
|
||||||
SidebarHeaderProps & {
|
hasBackground?: boolean;
|
||||||
hasBackground?: boolean;
|
}>;
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type History = {
|
export type History = {
|
||||||
stack: string[];
|
stack: string[];
|
||||||
@@ -97,10 +94,7 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
|||||||
data-has-background={environment.isDesktop && props.hasBackground}
|
data-has-background={environment.isDesktop && props.hasBackground}
|
||||||
>
|
>
|
||||||
<nav className={navStyle} data-testid="app-sidebar">
|
<nav className={navStyle} data-testid="app-sidebar">
|
||||||
<SidebarHeader
|
<SidebarHeader />
|
||||||
router={props.router}
|
|
||||||
generalShortcutsInfo={props.generalShortcutsInfo}
|
|
||||||
/>
|
|
||||||
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
<div className={navBodyStyle} data-testid="sliderBar-inner">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import { useHasScrollTop } from '@affine/component';
|
||||||
import * as ScrollArea from '@radix-ui/react-scroll-area';
|
import * as ScrollArea from '@radix-ui/react-scroll-area';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { type PropsWithChildren, useRef } from 'react';
|
import { type PropsWithChildren, useRef } from 'react';
|
||||||
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import { useHasScrollTop } from './use-has-scroll-top';
|
|
||||||
|
|
||||||
export { useHasScrollTop } from './use-has-scroll-top';
|
|
||||||
|
|
||||||
export function SidebarContainer({ children }: PropsWithChildren) {
|
export function SidebarContainer({ children }: PropsWithChildren) {
|
||||||
return <div className={clsx([styles.baseContainer])}>{children}</div>;
|
return <div className={clsx([styles.baseContainer])}>{children}</div>;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
|
||||||
|
import { NavigationButtons } from '../../../modules/navigation';
|
||||||
|
import { navHeaderStyle } from '../index.css';
|
||||||
|
import { appSidebarOpenAtom } from '../index.jotai';
|
||||||
|
import { SidebarSwitch } from './sidebar-switch';
|
||||||
|
|
||||||
|
export const SidebarHeader = () => {
|
||||||
|
const open = useAtomValue(appSidebarOpenAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={navHeaderStyle}
|
||||||
|
data-open={open}
|
||||||
|
data-is-macos-electron={environment.isDesktop && environment.isMacOs}
|
||||||
|
>
|
||||||
|
<SidebarSwitch show={open} />
|
||||||
|
<NavigationButtons />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from './sidebar-switch';
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import { IconButton, Tooltip } from '@affine/component';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { SidebarIcon } from '@blocksuite/icons';
|
import { SidebarIcon } from '@blocksuite/icons';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
import { IconButton } from '../../../ui/button';
|
|
||||||
import { Tooltip } from '../../../ui/tooltip';
|
|
||||||
import { appSidebarOpenAtom } from '../index.jotai';
|
import { appSidebarOpenAtom } from '../index.jotai';
|
||||||
import * as styles from './sidebar-switch.css';
|
import * as styles from './sidebar-switch.css';
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Scrollable } from '@affine/component';
|
import { Scrollable, useHasScrollTop } from '@affine/component';
|
||||||
import { useHasScrollTop } from '@affine/component/app-sidebar';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
type ForwardedRef,
|
type ForwardedRef,
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import {
|
|
||||||
appSidebarFloatingAtom,
|
|
||||||
appSidebarOpenAtom,
|
|
||||||
} from '@affine/component/app-sidebar';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { appSidebarFloatingAtom, appSidebarOpenAtom } from '../../app-sidebar';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
|
|
||||||
interface HeaderPros {
|
interface HeaderPros {
|
||||||
|
|||||||
@@ -47,13 +47,25 @@ export const WorkspaceModeFilterTab = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioButtonGroup value={value} onValueChange={handleValueChange}>
|
<RadioButtonGroup value={value} onValueChange={handleValueChange}>
|
||||||
<RadioButton spanStyle={styles.filterTab} value="docs">
|
<RadioButton
|
||||||
|
spanStyle={styles.filterTab}
|
||||||
|
value="docs"
|
||||||
|
data-testid="workspace-docs-button"
|
||||||
|
>
|
||||||
{t['com.affine.docs.header']()}
|
{t['com.affine.docs.header']()}
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
<RadioButton spanStyle={styles.filterTab} value="collections">
|
<RadioButton
|
||||||
|
spanStyle={styles.filterTab}
|
||||||
|
value="collections"
|
||||||
|
data-testid="workspace-collections-button"
|
||||||
|
>
|
||||||
{t['com.affine.collections.header']()}
|
{t['com.affine.collections.header']()}
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
<RadioButton spanStyle={styles.filterTab} value="tags">
|
<RadioButton
|
||||||
|
spanStyle={styles.filterTab}
|
||||||
|
value="tags"
|
||||||
|
data-testid="workspace-tags-button"
|
||||||
|
>
|
||||||
{t['Tags']()}
|
{t['Tags']()}
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
</RadioButtonGroup>
|
</RadioButtonGroup>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AnimatedCollectionsIcon, toast } from '@affine/component';
|
import { AnimatedCollectionsIcon, toast } from '@affine/component';
|
||||||
import { MenuLinkItem as SidebarMenuLinkItem } from '@affine/component/app-sidebar';
|
|
||||||
import { RenameModal } from '@affine/component/rename-modal';
|
import { RenameModal } from '@affine/component/rename-modal';
|
||||||
import { Button, IconButton } from '@affine/component/ui/button';
|
import { Button, IconButton } from '@affine/component/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +22,7 @@ import { getDropItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
|||||||
import { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '../../../../hooks/use-block-suite-page-meta';
|
||||||
import { Workbench } from '../../../../modules/workbench';
|
import { Workbench } from '../../../../modules/workbench';
|
||||||
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
|
import { WorkbenchLink } from '../../../../modules/workbench/view/workbench-link';
|
||||||
|
import { MenuLinkItem as SidebarMenuLinkItem } from '../../../app-sidebar';
|
||||||
import type { CollectionsListProps } from '../index';
|
import type { CollectionsListProps } from '../index';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { MenuItem as CollectionItem } from '@affine/component/app-sidebar';
|
|
||||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
@@ -11,6 +10,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
|
|
||||||
import { getDragItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
import { getDragItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||||
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
|
||||||
|
import { MenuItem as CollectionItem } from '../../../app-sidebar';
|
||||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||||
import { PostfixItem } from '../components/postfix-item';
|
import { PostfixItem } from '../components/postfix-item';
|
||||||
import { ReferencePage } from '../components/reference-page';
|
import { ReferencePage } from '../components/reference-page';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
|
||||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
@@ -8,6 +7,7 @@ import { PageRecordList, useLiveData, useService } from '@toeverything/infra';
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { MenuLinkItem } from '../../../app-sidebar';
|
||||||
import * as styles from '../favorite/styles.css';
|
import * as styles from '../favorite/styles.css';
|
||||||
import { PostfixItem } from './postfix-item';
|
import { PostfixItem } from './postfix-item';
|
||||||
export interface ReferencePageProps {
|
export interface ReferencePageProps {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
|
||||||
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
import { useBlockSuitePageReferences } from '@affine/core/hooks/use-block-suite-page-references';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||||
@@ -11,6 +10,7 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { getDragItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
import { getDragItemId } from '../../../../hooks/affine/use-sidebar-drag';
|
||||||
|
import { MenuLinkItem } from '../../../app-sidebar';
|
||||||
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
import { DragMenuItemOverlay } from '../components/drag-menu-item-overlay';
|
||||||
import { PostfixItem } from '../components/postfix-item';
|
import { PostfixItem } from '../components/postfix-item';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { MenuItem } from '@affine/component/app-sidebar';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ImportIcon } from '@blocksuite/icons';
|
import { ImportIcon } from '@blocksuite/icons';
|
||||||
|
|
||||||
import type { BlockSuiteWorkspace } from '../../shared';
|
import type { BlockSuiteWorkspace } from '../../shared';
|
||||||
|
import { MenuItem } from '../app-sidebar';
|
||||||
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||||
|
|
||||||
const ImportPage = ({
|
const ImportPage = ({
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
import { AnimatedDeleteIcon } from '@affine/component';
|
import { AnimatedDeleteIcon } from '@affine/component';
|
||||||
import {
|
|
||||||
AddPageButton,
|
|
||||||
AppDownloadButton,
|
|
||||||
AppSidebar,
|
|
||||||
appSidebarOpenAtom,
|
|
||||||
CategoryDivider,
|
|
||||||
MenuItem,
|
|
||||||
MenuLinkItem,
|
|
||||||
QuickSearchInput,
|
|
||||||
SidebarContainer,
|
|
||||||
SidebarScrollableContainer,
|
|
||||||
} from '@affine/component/app-sidebar';
|
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { CollectionService } from '@affine/core/modules/collection';
|
import { CollectionService } from '@affine/core/modules/collection';
|
||||||
@@ -23,19 +11,27 @@ import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
|||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { HTMLAttributes, ReactElement } from 'react';
|
import type { HTMLAttributes, ReactElement } from 'react';
|
||||||
import { forwardRef, Suspense, useCallback, useEffect, useMemo } from 'react';
|
import { forwardRef, Suspense, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { openWorkspaceListModalAtom } from '../../atoms';
|
import { openWorkspaceListModalAtom } from '../../atoms';
|
||||||
import { useHistoryAtom } from '../../atoms/history';
|
|
||||||
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
|
||||||
import { useDeleteCollectionInfo } from '../../hooks/affine/use-delete-collection-info';
|
import { useDeleteCollectionInfo } from '../../hooks/affine/use-delete-collection-info';
|
||||||
import { useGeneralShortcuts } from '../../hooks/affine/use-shortcuts';
|
|
||||||
import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
|
import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
|
||||||
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||||
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
|
|
||||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
import { Workbench } from '../../modules/workbench';
|
import { Workbench } from '../../modules/workbench';
|
||||||
import { WorkspaceSubPath } from '../../shared';
|
import {
|
||||||
|
AddPageButton,
|
||||||
|
AppDownloadButton,
|
||||||
|
AppSidebar,
|
||||||
|
appSidebarOpenAtom,
|
||||||
|
CategoryDivider,
|
||||||
|
MenuItem,
|
||||||
|
MenuLinkItem,
|
||||||
|
QuickSearchInput,
|
||||||
|
SidebarContainer,
|
||||||
|
SidebarScrollableContainer,
|
||||||
|
} from '../app-sidebar';
|
||||||
import {
|
import {
|
||||||
createEmptyCollection,
|
createEmptyCollection,
|
||||||
MoveToTrash,
|
MoveToTrash,
|
||||||
@@ -109,7 +105,6 @@ export const RootAppSidebar = ({
|
|||||||
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useAtom(
|
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useAtom(
|
||||||
openWorkspaceListModalAtom
|
openWorkspaceListModalAtom
|
||||||
);
|
);
|
||||||
const generalShortcutsInfo = useGeneralShortcuts();
|
|
||||||
const currentPath = useLiveData(useService(Workbench).location).pathname;
|
const currentPath = useLiveData(useService(Workbench).location).pathname;
|
||||||
|
|
||||||
const onClickNewPage = useAsyncCallback(async () => {
|
const onClickNewPage = useAsyncCallback(async () => {
|
||||||
@@ -133,9 +128,6 @@ export const RootAppSidebar = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const navigateHelper = useNavigateHelper();
|
const navigateHelper = useNavigateHelper();
|
||||||
const backToAll = useCallback(() => {
|
|
||||||
navigateHelper.jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
|
|
||||||
}, [currentWorkspace.id, navigateHelper]);
|
|
||||||
// Listen to the "New Page" action from the menu
|
// Listen to the "New Page" action from the menu
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (environment.isDesktop) {
|
if (environment.isDesktop) {
|
||||||
@@ -153,19 +145,6 @@ export const RootAppSidebar = ({
|
|||||||
}
|
}
|
||||||
}, [sidebarOpen]);
|
}, [sidebarOpen]);
|
||||||
|
|
||||||
const [history, setHistory] = useHistoryAtom();
|
|
||||||
const router = useMemo(() => {
|
|
||||||
return {
|
|
||||||
forward: () => {
|
|
||||||
setHistory(true);
|
|
||||||
},
|
|
||||||
back: () => {
|
|
||||||
setHistory(false);
|
|
||||||
},
|
|
||||||
history,
|
|
||||||
};
|
|
||||||
}, [history, setHistory]);
|
|
||||||
|
|
||||||
const dropItemId = getDropItemId('trash');
|
const dropItemId = getDropItemId('trash');
|
||||||
const trashDroppable = useDroppable({
|
const trashDroppable = useDroppable({
|
||||||
id: dropItemId,
|
id: dropItemId,
|
||||||
@@ -173,7 +152,6 @@ export const RootAppSidebar = ({
|
|||||||
const closeUserWorkspaceList = useCallback(() => {
|
const closeUserWorkspaceList = useCallback(() => {
|
||||||
setOpenUserWorkspaceList(false);
|
setOpenUserWorkspaceList(false);
|
||||||
}, [setOpenUserWorkspaceList]);
|
}, [setOpenUserWorkspaceList]);
|
||||||
useRegisterBrowserHistoryCommands(router.back, router.forward);
|
|
||||||
const userInfo = useDeleteCollectionInfo();
|
const userInfo = useDeleteCollectionInfo();
|
||||||
|
|
||||||
const collection = useService(CollectionService);
|
const collection = useService(CollectionService);
|
||||||
@@ -199,7 +177,6 @@ export const RootAppSidebar = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
router={router}
|
|
||||||
hasBackground={
|
hasBackground={
|
||||||
!(
|
!(
|
||||||
appSettings.enableBlurBackground &&
|
appSettings.enableBlurBackground &&
|
||||||
@@ -207,7 +184,6 @@ export const RootAppSidebar = ({
|
|||||||
environment.isMacOs
|
environment.isMacOs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
generalShortcutsInfo={generalShortcutsInfo}
|
|
||||||
>
|
>
|
||||||
<MoveToTrash.ConfirmModal
|
<MoveToTrash.ConfirmModal
|
||||||
open={trashConfirmOpen}
|
open={trashConfirmOpen}
|
||||||
@@ -249,7 +225,6 @@ export const RootAppSidebar = ({
|
|||||||
icon={<FolderIcon />}
|
icon={<FolderIcon />}
|
||||||
active={allPageActive}
|
active={allPageActive}
|
||||||
path={paths.all(currentWorkspaceId)}
|
path={paths.all(currentWorkspaceId)}
|
||||||
onClick={backToAll}
|
|
||||||
>
|
>
|
||||||
<span data-testid="all-pages">
|
<span data-testid="all-pages">
|
||||||
{t['com.affine.workspaceSubPath.all']()}
|
{t['com.affine.workspaceSubPath.all']()}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { MenuItem } from '@affine/component/app-sidebar';
|
|
||||||
import {
|
import {
|
||||||
useJournalInfoHelper,
|
useJournalInfoHelper,
|
||||||
useJournalRouteHelper,
|
useJournalRouteHelper,
|
||||||
@@ -9,6 +8,8 @@ import { TodayIcon, TomorrowIcon, YesterdayIcon } from '@blocksuite/icons';
|
|||||||
import { Doc, useServiceOptional } from '@toeverything/infra';
|
import { Doc, useServiceOptional } from '@toeverything/infra';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { MenuItem } from '../app-sidebar';
|
||||||
|
|
||||||
interface AppSidebarJournalButtonProps {
|
interface AppSidebarJournalButtonProps {
|
||||||
workspace: BlockSuiteWorkspace;
|
workspace: BlockSuiteWorkspace;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AppUpdaterButton } from '@affine/component/app-sidebar/app-updater-button';
|
|
||||||
import { useAppUpdater } from '@affine/core/hooks/use-app-updater';
|
import { useAppUpdater } from '@affine/core/hooks/use-app-updater';
|
||||||
|
|
||||||
|
import { AppUpdaterButton } from '../app-sidebar';
|
||||||
|
|
||||||
export const UpdaterButton = () => {
|
export const UpdaterButton = () => {
|
||||||
const appUpdater = useAppUpdater();
|
const appUpdater = useAppUpdater();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
|
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { appSidebarOpenAtom } from '../../components/app-sidebar';
|
||||||
|
|
||||||
export function useSwitchSidebarStatus() {
|
export function useSwitchSidebarStatus() {
|
||||||
const [isOpened, setOpened] = useAtom(appSidebarOpenAtom);
|
const [isOpened, setOpened] = useAtom(appSidebarOpenAtom);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
import {
|
|
||||||
AppSidebarFallback,
|
|
||||||
appSidebarResizingAtom,
|
|
||||||
} from '@affine/component/app-sidebar';
|
|
||||||
import { MainContainer, WorkspaceFallback } from '@affine/component/workspace';
|
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
|
import { useWorkspaceStatus } from '@affine/core/hooks/use-workspace-status';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
@@ -26,12 +21,17 @@ import { Map as YMap } from 'yjs';
|
|||||||
import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
|
import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
|
||||||
import { AppContainer } from '../components/affine/app-container';
|
import { AppContainer } from '../components/affine/app-container';
|
||||||
import { SyncAwareness } from '../components/affine/awareness';
|
import { SyncAwareness } from '../components/affine/awareness';
|
||||||
|
import {
|
||||||
|
AppSidebarFallback,
|
||||||
|
appSidebarResizingAtom,
|
||||||
|
} from '../components/app-sidebar';
|
||||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||||
import {
|
import {
|
||||||
type DraggableTitleCellData,
|
type DraggableTitleCellData,
|
||||||
PageListDragOverlay,
|
PageListDragOverlay,
|
||||||
} from '../components/page-list';
|
} from '../components/page-list';
|
||||||
import { RootAppSidebar } from '../components/root-app-sidebar';
|
import { RootAppSidebar } from '../components/root-app-sidebar';
|
||||||
|
import { MainContainer, WorkspaceFallback } from '../components/workspace';
|
||||||
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
||||||
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
||||||
import { useSidebarDrag } from '../hooks/affine/use-sidebar-drag';
|
import { useSidebarDrag } from '../hooks/affine/use-sidebar-drag';
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { LiveData } from '@toeverything/infra';
|
||||||
|
import type { Location } from 'history';
|
||||||
|
import { Observable, switchMap } from 'rxjs';
|
||||||
|
|
||||||
|
import type { Workbench } from '../../workbench';
|
||||||
|
|
||||||
|
export class Navigator {
|
||||||
|
constructor(private readonly workbench: Workbench) {}
|
||||||
|
|
||||||
|
private readonly history = this.workbench.activeView.map(
|
||||||
|
view => view.history
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly location = LiveData.from(
|
||||||
|
this.history.pipe(
|
||||||
|
switchMap(
|
||||||
|
history =>
|
||||||
|
new Observable<{ index: number; entries: Location[] }>(subscriber => {
|
||||||
|
subscriber.next({ index: history.index, entries: history.entries });
|
||||||
|
return history.listen(() => {
|
||||||
|
subscriber.next({
|
||||||
|
index: history.index,
|
||||||
|
entries: history.entries,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
{ index: 0, entries: [] }
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly backable = this.location.map(
|
||||||
|
({ index, entries }) => index > 0 && entries.length > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly forwardable = this.location.map(
|
||||||
|
({ index, entries }) => index < entries.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
back() {
|
||||||
|
if (!environment.isDesktop) {
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
|
this.history.value.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forward() {
|
||||||
|
if (!environment.isDesktop) {
|
||||||
|
window.history.forward();
|
||||||
|
} else {
|
||||||
|
this.history.value.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/frontend/core/src/modules/navigation/index.ts
Normal file
2
packages/frontend/core/src/modules/navigation/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { Navigator } from './entities/navigator';
|
||||||
|
export { NavigationButtons } from './view/navigation-buttons';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
columnGap: '32px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const button = style({
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { IconButton, Tooltip } from '@affine/component';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
|
import { useLiveData } from '@toeverything/infra/livedata';
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useGeneralShortcuts } from '../../../hooks/affine/use-shortcuts';
|
||||||
|
import { Navigator } from '../entities/navigator';
|
||||||
|
import * as styles from './navigation-buttons.css';
|
||||||
|
import { useRegisterNavigationCommands } from './use-register-navigation-commands';
|
||||||
|
|
||||||
|
export const NavigationButtons = () => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
const shortcuts = useGeneralShortcuts().shortcuts;
|
||||||
|
|
||||||
|
useRegisterNavigationCommands();
|
||||||
|
|
||||||
|
const shortcutsObject = useMemo(() => {
|
||||||
|
const goBack = t['com.affine.keyboardShortcuts.goBack']();
|
||||||
|
const goBackShortcut = shortcuts?.[goBack];
|
||||||
|
|
||||||
|
const goForward = t['com.affine.keyboardShortcuts.goForward']();
|
||||||
|
const goForwardShortcut = shortcuts?.[goForward];
|
||||||
|
return {
|
||||||
|
goBack,
|
||||||
|
goBackShortcut,
|
||||||
|
goForward,
|
||||||
|
goForwardShortcut,
|
||||||
|
};
|
||||||
|
}, [shortcuts, t]);
|
||||||
|
|
||||||
|
const navigator = useService(Navigator);
|
||||||
|
|
||||||
|
const backable = useLiveData(navigator.backable);
|
||||||
|
const forwardable = useLiveData(navigator.forwardable);
|
||||||
|
|
||||||
|
const handleBack = useCallback(() => {
|
||||||
|
navigator.back();
|
||||||
|
}, [navigator]);
|
||||||
|
|
||||||
|
const handleForward = useCallback(() => {
|
||||||
|
navigator.forward();
|
||||||
|
}, [navigator]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cb = (event: MouseEvent) => {
|
||||||
|
if (event.button === 3 || event.button === 4) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (event.button === 3) {
|
||||||
|
navigator.back();
|
||||||
|
} else {
|
||||||
|
navigator.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('mouseup', cb);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mouseup', cb);
|
||||||
|
};
|
||||||
|
}, [navigator]);
|
||||||
|
|
||||||
|
if (!environment.isDesktop) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Tooltip
|
||||||
|
content={`${shortcutsObject.goBack} ${shortcutsObject.goBackShortcut}`}
|
||||||
|
side="bottom"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
className={styles.button}
|
||||||
|
data-testid="app-navigation-button-back"
|
||||||
|
disabled={!backable}
|
||||||
|
onClick={handleBack}
|
||||||
|
>
|
||||||
|
<ArrowLeftSmallIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
content={`${shortcutsObject.goForward} ${shortcutsObject.goForwardShortcut}`}
|
||||||
|
side="bottom"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
className={styles.button}
|
||||||
|
data-testid="app-navigation-button-forward"
|
||||||
|
disabled={!forwardable}
|
||||||
|
onClick={handleForward}
|
||||||
|
>
|
||||||
|
<ArrowRightSmallIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,12 +2,13 @@ import {
|
|||||||
PreconditionStrategy,
|
PreconditionStrategy,
|
||||||
registerAffineCommand,
|
registerAffineCommand,
|
||||||
} from '@toeverything/infra/command';
|
} from '@toeverything/infra/command';
|
||||||
|
import { useService } from '@toeverything/infra/di';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export function useRegisterBrowserHistoryCommands(
|
import { Navigator } from '../entities/navigator';
|
||||||
back: () => unknown,
|
|
||||||
forward: () => unknown
|
export function useRegisterNavigationCommands() {
|
||||||
) {
|
const navigator = useService(Navigator);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubs: Array<() => void> = [];
|
const unsubs: Array<() => void> = [];
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export function useRegisterBrowserHistoryCommands(
|
|||||||
binding: '$mod+[',
|
binding: '$mod+[',
|
||||||
},
|
},
|
||||||
run() {
|
run() {
|
||||||
back();
|
navigator.back();
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -37,7 +38,7 @@ export function useRegisterBrowserHistoryCommands(
|
|||||||
binding: '$mod+]',
|
binding: '$mod+]',
|
||||||
},
|
},
|
||||||
run() {
|
run() {
|
||||||
forward();
|
navigator.forward();
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -45,5 +46,5 @@ export function useRegisterBrowserHistoryCommands(
|
|||||||
return () => {
|
return () => {
|
||||||
unsubs.forEach(unsub => unsub());
|
unsubs.forEach(unsub => unsub());
|
||||||
};
|
};
|
||||||
}, [back, forward]);
|
}, [navigator]);
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
LocalStorageGlobalCache,
|
LocalStorageGlobalCache,
|
||||||
LocalStorageGlobalState,
|
LocalStorageGlobalState,
|
||||||
} from './infra-web/storage';
|
} from './infra-web/storage';
|
||||||
|
import { Navigator } from './navigation';
|
||||||
import { RightSidebar } from './right-sidebar/entities/right-sidebar';
|
import { RightSidebar } from './right-sidebar/entities/right-sidebar';
|
||||||
import { Workbench } from './workbench';
|
import { Workbench } from './workbench';
|
||||||
import {
|
import {
|
||||||
@@ -24,6 +25,7 @@ export function configureBusinessServices(services: ServiceCollection) {
|
|||||||
services
|
services
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.add(Workbench)
|
.add(Workbench)
|
||||||
|
.add(Navigator, [Workbench])
|
||||||
.add(RightSidebar)
|
.add(RightSidebar)
|
||||||
.add(WorkspacePropertiesAdapter, [Workspace])
|
.add(WorkspacePropertiesAdapter, [Workspace])
|
||||||
.add(CollectionService, [Workspace])
|
.add(CollectionService, [Workspace])
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import { LiveData } from '@toeverything/infra';
|
import { LiveData } from '@toeverything/infra';
|
||||||
import type { Location, To } from 'history';
|
import type { Location, To } from 'history';
|
||||||
import { createMemoryHistory } from 'history';
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { createIsland } from '../../../utils/island';
|
import { createIsland } from '../../../utils/island';
|
||||||
|
import { createNavigableHistory } from '../../../utils/navigable-history';
|
||||||
|
|
||||||
export class View {
|
export class View {
|
||||||
|
constructor(defaultPath: To = { pathname: '/all' }) {
|
||||||
|
this.history = createNavigableHistory({
|
||||||
|
initialEntries: [defaultPath],
|
||||||
|
initialIndex: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
id = nanoid();
|
id = nanoid();
|
||||||
|
|
||||||
history = createMemoryHistory();
|
history = createNavigableHistory({
|
||||||
|
initialEntries: ['/all'],
|
||||||
|
initialIndex: 0,
|
||||||
|
});
|
||||||
|
|
||||||
location = LiveData.from<Location>(
|
location = LiveData.from<Location>(
|
||||||
new Observable(subscriber => {
|
new Observable(subscriber => {
|
||||||
@@ -20,6 +30,17 @@ export class View {
|
|||||||
}),
|
}),
|
||||||
this.history.location
|
this.history.location
|
||||||
);
|
);
|
||||||
|
|
||||||
|
entries = LiveData.from<Location[]>(
|
||||||
|
new Observable(subscriber => {
|
||||||
|
subscriber.next(this.history.entries);
|
||||||
|
return this.history.listen(() => {
|
||||||
|
subscriber.next(this.history.entries);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
this.history.entries
|
||||||
|
);
|
||||||
|
|
||||||
size = new LiveData(100);
|
size = new LiveData(100);
|
||||||
|
|
||||||
header = createIsland();
|
header = createIsland();
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export class Workbench {
|
|||||||
this.activeViewIndex.next(index);
|
this.activeViewIndex.next(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
createView(at: WorkbenchPosition = 'beside') {
|
createView(at: WorkbenchPosition = 'beside', defaultLocation: To) {
|
||||||
const view = new View();
|
const view = new View(defaultLocation);
|
||||||
const newViews = [...this.views.value];
|
const newViews = [...this.views.value];
|
||||||
newViews.splice(this.indexAt(at), 0, view);
|
newViews.splice(this.indexAt(at), 0, view);
|
||||||
this.views.next(newViews);
|
this.views.next(newViews);
|
||||||
@@ -44,16 +44,17 @@ export class Workbench {
|
|||||||
) {
|
) {
|
||||||
let view = this.viewAt(at);
|
let view = this.viewAt(at);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
const newIndex = this.createView(at);
|
const newIndex = this.createView(at, to);
|
||||||
view = this.viewAt(newIndex);
|
view = this.viewAt(newIndex);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
throw new Unreachable();
|
throw new Unreachable();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (replaceHistory) {
|
|
||||||
view.history.replace(to);
|
|
||||||
} else {
|
} else {
|
||||||
view.history.push(to);
|
if (replaceHistory) {
|
||||||
|
view.history.replace(to);
|
||||||
|
} else {
|
||||||
|
view.history.push(to);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ export function useBindWorkbenchToDesktopRouter(
|
|||||||
if (newLocation === null) {
|
if (newLocation === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
workbench.location.value.pathname === newLocation.pathname &&
|
||||||
|
workbench.location.value.search === newLocation.search &&
|
||||||
|
workbench.location.value.hash === newLocation.hash
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
workbench.open(newLocation);
|
workbench.open(newLocation);
|
||||||
}, [basename, browserLocation, workbench]);
|
}, [basename, browserLocation, workbench]);
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { IconButton } from '@affine/component';
|
||||||
import {
|
|
||||||
appSidebarOpenAtom,
|
|
||||||
SidebarSwitch,
|
|
||||||
} from '@affine/component/app-sidebar';
|
|
||||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||||
import { RightSidebarIcon } from '@blocksuite/icons';
|
import { RightSidebarIcon } from '@blocksuite/icons';
|
||||||
import { useLiveData } from '@toeverything/infra';
|
import { useLiveData } from '@toeverything/infra';
|
||||||
@@ -10,6 +6,10 @@ import { useService } from '@toeverything/infra/di';
|
|||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { Suspense, useCallback } from 'react';
|
import { Suspense, useCallback } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
appSidebarOpenAtom,
|
||||||
|
SidebarSwitch,
|
||||||
|
} from '../../../components/app-sidebar';
|
||||||
import { RightSidebar } from '../../right-sidebar';
|
import { RightSidebar } from '../../right-sidebar';
|
||||||
import * as styles from './route-container.css';
|
import * as styles from './route-container.css';
|
||||||
import { useView } from './use-view';
|
import { useView } from './use-view';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
|
||||||
import { WorkspaceManager } from '@toeverything/infra';
|
import { WorkspaceManager } from '@toeverything/infra';
|
||||||
import { WorkspaceListService } from '@toeverything/infra';
|
import { WorkspaceListService } from '@toeverything/infra';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
@@ -9,6 +8,7 @@ import { type LoaderFunction, redirect } from 'react-router-dom';
|
|||||||
|
|
||||||
import { createFirstAppData } from '../bootstrap/first-app-data';
|
import { createFirstAppData } from '../bootstrap/first-app-data';
|
||||||
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
||||||
|
import { WorkspaceFallback } from '../components/workspace';
|
||||||
import { appConfigStorage } from '../hooks/use-app-config-storage';
|
import { appConfigStorage } from '../hooks/use-app-config-storage';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { WorkspaceSubPath } from '../shared';
|
import { WorkspaceSubPath } from '../shared';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Scrollable } from '@affine/component';
|
import { Scrollable } from '@affine/component';
|
||||||
import { MainContainer } from '@affine/component/workspace';
|
|
||||||
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '@affine/core/hooks/affine/use-current-login-status';
|
||||||
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
|
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
@@ -39,6 +38,7 @@ import {
|
|||||||
import { AppContainer } from '../../components/affine/app-container';
|
import { AppContainer } from '../../components/affine/app-container';
|
||||||
import { PageDetailEditor } from '../../components/page-detail-editor';
|
import { PageDetailEditor } from '../../components/page-detail-editor';
|
||||||
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
||||||
|
import { MainContainer } from '../../components/workspace';
|
||||||
import { CurrentWorkspaceService } from '../../modules/workspace';
|
import { CurrentWorkspaceService } from '../../modules/workspace';
|
||||||
import * as styles from './share-detail-page.css';
|
import * as styles from './share-detail-page.css';
|
||||||
import { ShareFooter } from './share-footer';
|
import { ShareFooter } from './share-footer';
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const body = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: 1,
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
@@ -18,6 +18,7 @@ import { CollectionService } from '../../../modules/collection';
|
|||||||
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
import { ViewBodyIsland, ViewHeaderIsland } from '../../../modules/workbench';
|
||||||
import { EmptyCollectionList } from '../page-list-empty';
|
import { EmptyCollectionList } from '../page-list-empty';
|
||||||
import { AllCollectionHeader } from './header';
|
import { AllCollectionHeader } from './header';
|
||||||
|
import * as styles from './index.css';
|
||||||
|
|
||||||
export const AllCollection = () => {
|
export const AllCollection = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
@@ -65,25 +66,27 @@ export const AllCollection = () => {
|
|||||||
/>
|
/>
|
||||||
</ViewHeaderIsland>
|
</ViewHeaderIsland>
|
||||||
<ViewBodyIsland>
|
<ViewBodyIsland>
|
||||||
{collectionMetas.length > 0 ? (
|
<div className={styles.body}>
|
||||||
<VirtualizedCollectionList
|
{collectionMetas.length > 0 ? (
|
||||||
collections={collections}
|
<VirtualizedCollectionList
|
||||||
collectionMetas={collectionMetas}
|
collections={collections}
|
||||||
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
|
collectionMetas={collectionMetas}
|
||||||
node={node}
|
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
|
||||||
config={config}
|
node={node}
|
||||||
handleCreateCollection={handleCreateCollection}
|
config={config}
|
||||||
/>
|
handleCreateCollection={handleCreateCollection}
|
||||||
) : (
|
/>
|
||||||
<EmptyCollectionList
|
) : (
|
||||||
heading={
|
<EmptyCollectionList
|
||||||
<CollectionListHeader
|
heading={
|
||||||
node={node}
|
<CollectionListHeader
|
||||||
onCreate={handleCreateCollection}
|
node={node}
|
||||||
/>
|
onCreate={handleCreateCollection}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</ViewBodyIsland>
|
</ViewBodyIsland>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { InlineEditHandle } from '@affine/component';
|
import type { InlineEditHandle } from '@affine/component';
|
||||||
import { appSidebarFloatingAtom } from '@affine/component/app-sidebar';
|
|
||||||
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
import { FavoriteButton } from '@affine/core/components/blocksuite/block-suite-header/favorite';
|
||||||
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
import { JournalWeekDatePicker } from '@affine/core/components/blocksuite/block-suite-header/journal/date-picker';
|
||||||
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
import { JournalTodayButton } from '@affine/core/components/blocksuite/block-suite-header/journal/today-button';
|
||||||
@@ -12,6 +11,7 @@ import { useAtomValue } from 'jotai';
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
import { SharePageButton } from '../../../components/affine/share-page-modal';
|
||||||
|
import { appSidebarFloatingAtom } from '../../../components/app-sidebar';
|
||||||
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
|
import { BlocksuiteHeaderTitle } from '../../../components/blocksuite/block-suite-header/title/index';
|
||||||
import { HeaderDivider } from '../../../components/pure/header';
|
import { HeaderDivider } from '../../../components/pure/header';
|
||||||
import * as styles from './detail-page-header.css';
|
import * as styles from './detail-page-header.css';
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
type ReactElement,
|
type ReactElement,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
@@ -269,7 +270,7 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
|
|||||||
|
|
||||||
const [page, setPage] = useState<Doc | null>(null);
|
const [page, setPage] = useState<Doc | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!pageRecord) {
|
if (!pageRecord) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
|
||||||
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -22,6 +21,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
|
|
||||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||||
import { HubIsland } from '../../components/affine/hub-island';
|
import { HubIsland } from '../../components/affine/hub-island';
|
||||||
|
import { WorkspaceFallback } from '../../components/workspace';
|
||||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||||
import { RightSidebarContainer } from '../../modules/right-sidebar';
|
import { RightSidebarContainer } from '../../modules/right-sidebar';
|
||||||
import { WorkbenchRoot } from '../../modules/workbench';
|
import { WorkbenchRoot } from '../../modules/workbench';
|
||||||
|
|||||||
221
packages/frontend/core/src/utils/navigable-history.ts
Normal file
221
packages/frontend/core/src/utils/navigable-history.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import type { Blocker, Listener, Location, To } from 'history';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
createPath,
|
||||||
|
type MemoryHistory,
|
||||||
|
type MemoryHistoryOptions,
|
||||||
|
parsePath,
|
||||||
|
} from 'history';
|
||||||
|
|
||||||
|
export interface NavigableHistory extends MemoryHistory {
|
||||||
|
entries: Location[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as `createMemoryHistory` from `history` package, but with additional `entries` property.
|
||||||
|
*
|
||||||
|
* Original `MemoryHistory` does not have `entries` property, so we can't get `backable` and `forwardable` state which
|
||||||
|
* is needed for implementing back and forward buttons.
|
||||||
|
*
|
||||||
|
* @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#creatememoryhistory
|
||||||
|
*/
|
||||||
|
export function createNavigableHistory(
|
||||||
|
options: MemoryHistoryOptions = {}
|
||||||
|
): NavigableHistory {
|
||||||
|
const { initialEntries = ['/'], initialIndex } = options;
|
||||||
|
const entries: Location[] = initialEntries.map(entry => {
|
||||||
|
const location = Object.freeze<Location>({
|
||||||
|
pathname: '/',
|
||||||
|
search: '',
|
||||||
|
hash: '',
|
||||||
|
state: null,
|
||||||
|
key: createKey(),
|
||||||
|
...(typeof entry === 'string' ? parsePath(entry) : entry),
|
||||||
|
});
|
||||||
|
|
||||||
|
warning(
|
||||||
|
location.pathname.charAt(0) === '/',
|
||||||
|
`Relative pathnames are not supported in createMemoryHistory({ initialEntries }) (invalid entry: ${JSON.stringify(
|
||||||
|
entry
|
||||||
|
)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
return location;
|
||||||
|
});
|
||||||
|
let index = clamp(
|
||||||
|
initialIndex == null ? entries.length - 1 : initialIndex,
|
||||||
|
0,
|
||||||
|
entries.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
let action = Action.Pop;
|
||||||
|
let location = entries[index];
|
||||||
|
const listeners = createEvents<Listener>();
|
||||||
|
const blockers = createEvents<Blocker>();
|
||||||
|
|
||||||
|
function createHref(to: To) {
|
||||||
|
return typeof to === 'string' ? to : createPath(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextLocation(to: To, state: any = null): Location {
|
||||||
|
return Object.freeze<Location>({
|
||||||
|
pathname: location.pathname,
|
||||||
|
search: '',
|
||||||
|
hash: '',
|
||||||
|
...(typeof to === 'string' ? parsePath(to) : to),
|
||||||
|
state,
|
||||||
|
key: createKey(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowTx(action: Action, location: Location, retry: () => void) {
|
||||||
|
return (
|
||||||
|
!blockers.length || (blockers.call({ action, location, retry }), false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTx(nextAction: Action, nextLocation: Location) {
|
||||||
|
action = nextAction;
|
||||||
|
location = nextLocation;
|
||||||
|
listeners.call({ action, location });
|
||||||
|
}
|
||||||
|
|
||||||
|
function push(to: To, state?: any) {
|
||||||
|
const nextAction = Action.Push;
|
||||||
|
const nextLocation = getNextLocation(to, state);
|
||||||
|
function retry() {
|
||||||
|
push(to, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(
|
||||||
|
location.pathname.charAt(0) === '/',
|
||||||
|
`Relative pathnames are not supported in memory history.push(${JSON.stringify(
|
||||||
|
to
|
||||||
|
)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowTx(nextAction, nextLocation, retry)) {
|
||||||
|
index += 1;
|
||||||
|
entries.splice(index, entries.length, nextLocation);
|
||||||
|
applyTx(nextAction, nextLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replace(to: To, state?: any) {
|
||||||
|
const nextAction = Action.Replace;
|
||||||
|
const nextLocation = getNextLocation(to, state);
|
||||||
|
function retry() {
|
||||||
|
replace(to, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(
|
||||||
|
location.pathname.charAt(0) === '/',
|
||||||
|
`Relative pathnames are not supported in memory history.replace(${JSON.stringify(
|
||||||
|
to
|
||||||
|
)})`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowTx(nextAction, nextLocation, retry)) {
|
||||||
|
entries[index] = nextLocation;
|
||||||
|
applyTx(nextAction, nextLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function go(delta: number) {
|
||||||
|
const nextIndex = clamp(index + delta, 0, entries.length - 1);
|
||||||
|
const nextAction = Action.Pop;
|
||||||
|
const nextLocation = entries[nextIndex];
|
||||||
|
function retry() {
|
||||||
|
go(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowTx(nextAction, nextLocation, retry)) {
|
||||||
|
index = nextIndex;
|
||||||
|
applyTx(nextAction, nextLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const history: NavigableHistory = {
|
||||||
|
get index() {
|
||||||
|
return index;
|
||||||
|
},
|
||||||
|
get action() {
|
||||||
|
return action;
|
||||||
|
},
|
||||||
|
get location() {
|
||||||
|
return location;
|
||||||
|
},
|
||||||
|
get entries() {
|
||||||
|
return entries;
|
||||||
|
},
|
||||||
|
createHref,
|
||||||
|
push,
|
||||||
|
replace,
|
||||||
|
go,
|
||||||
|
back() {
|
||||||
|
go(-1);
|
||||||
|
},
|
||||||
|
forward() {
|
||||||
|
go(1);
|
||||||
|
},
|
||||||
|
listen(listener) {
|
||||||
|
return listeners.push(listener);
|
||||||
|
},
|
||||||
|
block(blocker) {
|
||||||
|
return blockers.push(blocker);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createKey() {
|
||||||
|
return Math.random().toString(36).substr(2, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning(cond: any, message: string) {
|
||||||
|
if (!cond) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
if (typeof console !== 'undefined') console.warn(message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Welcome to debugging history!
|
||||||
|
//
|
||||||
|
// This error is thrown as a convenience so you can more easily
|
||||||
|
// find the source for a warning that appears in the console by
|
||||||
|
// enabling "pause on exceptions" in your JavaScript debugger.
|
||||||
|
throw new Error(message);
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(n: number, lowerBound: number, upperBound: number) {
|
||||||
|
return Math.min(Math.max(n, lowerBound), upperBound);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Events<F> = {
|
||||||
|
length: number;
|
||||||
|
push: (fn: F) => () => void;
|
||||||
|
call: (arg: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
function createEvents<F extends Function>(): Events<F> {
|
||||||
|
let handlers: F[] = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
get length() {
|
||||||
|
return handlers.length;
|
||||||
|
},
|
||||||
|
push(fn: F) {
|
||||||
|
handlers.push(fn);
|
||||||
|
return function () {
|
||||||
|
handlers = handlers.filter(handler => handler !== fn);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
call(arg) {
|
||||||
|
handlers.forEach(fn => fn && fn(arg));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -62,13 +62,13 @@ test('app sidebar router forward/back', async ({ page }) => {
|
|||||||
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test3');
|
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test3');
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
|
await page.click('[data-testid="app-navigation-button-back"]');
|
||||||
await page.click('[data-testid="app-sidebar-arrow-button-back"]');
|
await page.click('[data-testid="app-navigation-button-back"]');
|
||||||
{
|
{
|
||||||
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test1');
|
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test1');
|
||||||
}
|
}
|
||||||
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
|
await page.click('[data-testid="app-navigation-button-forward"]');
|
||||||
await page.click('[data-testid="app-sidebar-arrow-button-forward"]');
|
await page.click('[data-testid="app-navigation-button-forward"]');
|
||||||
{
|
{
|
||||||
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test3');
|
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test3');
|
||||||
}
|
}
|
||||||
|
|||||||
44
tests/affine-local/e2e/navigation.spec.ts
Normal file
44
tests/affine-local/e2e/navigation.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { test } from '@affine-test/kit/playwright';
|
||||||
|
import { withCtrlOrMeta } from '@affine-test/kit/utils/keyboard';
|
||||||
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
|
import {
|
||||||
|
clickNewPageButton,
|
||||||
|
getBlockSuiteEditorTitle,
|
||||||
|
waitForEditorLoad,
|
||||||
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
|
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
|
||||||
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const historyShortcut = async (page: Page, command: 'goBack' | 'goForward') => {
|
||||||
|
await withCtrlOrMeta(page, () =>
|
||||||
|
page.keyboard.press(command === 'goBack' ? '[' : ']', { delay: 300 })
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('back and forward buttons', async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('app-navigation-button-back')).toBeHidden();
|
||||||
|
await expect(page.getByTestId('app-navigation-button-forward')).toBeHidden();
|
||||||
|
|
||||||
|
await clickNewPageButton(page);
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
const title = getBlockSuiteEditorTitle(page);
|
||||||
|
await title.fill('test1');
|
||||||
|
|
||||||
|
await clickSideBarAllPageButton(page);
|
||||||
|
|
||||||
|
await page.getByTestId('workspace-collections-button').click({ delay: 50 });
|
||||||
|
await page.waitForURL(url => url.pathname.endsWith('collection'));
|
||||||
|
await page.getByTestId('workspace-tags-button').click({ delay: 50 });
|
||||||
|
await page.waitForURL(url => url.pathname.endsWith('tag'));
|
||||||
|
|
||||||
|
await page.goBack();
|
||||||
|
await page.waitForURL(url => url.pathname.endsWith('collection'));
|
||||||
|
await page.goBack();
|
||||||
|
await page.waitForURL(url => url.pathname.endsWith('all'));
|
||||||
|
await historyShortcut(page, 'goBack');
|
||||||
|
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
await expect(getBlockSuiteEditorTitle(page)).toHaveText('test1');
|
||||||
|
});
|
||||||
@@ -3,16 +3,16 @@ import {
|
|||||||
AppSidebarFallback,
|
AppSidebarFallback,
|
||||||
appSidebarOpenAtom,
|
appSidebarOpenAtom,
|
||||||
SidebarSwitch,
|
SidebarSwitch,
|
||||||
} from '@affine/component/app-sidebar';
|
} from '@affine/core/components/app-sidebar';
|
||||||
import { AddPageButton } from '@affine/component/app-sidebar';
|
import { AddPageButton } from '@affine/core/components/app-sidebar';
|
||||||
import { CategoryDivider } from '@affine/component/app-sidebar';
|
import { CategoryDivider } from '@affine/core/components/app-sidebar';
|
||||||
import { navHeaderStyle } from '@affine/component/app-sidebar';
|
import { navHeaderStyle } from '@affine/core/components/app-sidebar';
|
||||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
import { MenuLinkItem } from '@affine/core/components/app-sidebar';
|
||||||
import { QuickSearchInput } from '@affine/component/app-sidebar';
|
import { QuickSearchInput } from '@affine/core/components/app-sidebar';
|
||||||
import {
|
import {
|
||||||
SidebarContainer,
|
SidebarContainer,
|
||||||
SidebarScrollableContainer,
|
SidebarScrollableContainer,
|
||||||
} from '@affine/component/app-sidebar';
|
} from '@affine/core/components/app-sidebar';
|
||||||
import { DeleteTemporarilyIcon, SettingsIcon } from '@blocksuite/icons';
|
import { DeleteTemporarilyIcon, SettingsIcon } from '@blocksuite/icons';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type AddPageButtonProps,
|
type AddPageButtonProps,
|
||||||
AppUpdaterButton,
|
AppUpdaterButton,
|
||||||
} from '@affine/component/app-sidebar';
|
} from '@affine/core/components/app-sidebar';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user