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:
EYHN
2024-03-05 07:01:24 +00:00
parent b06aeb22dd
commit 7c76c25a9c
77 changed files with 625 additions and 349 deletions

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,2 @@
export { Navigator } from './entities/navigator';
export { NavigationButtons } from './view/navigation-buttons';

View File

@@ -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,
});

View File

@@ -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>
);
};

View File

@@ -0,0 +1,50 @@
import {
PreconditionStrategy,
registerAffineCommand,
} from '@toeverything/infra/command';
import { useService } from '@toeverything/infra/di';
import { useEffect } from 'react';
import { Navigator } from '../entities/navigator';
export function useRegisterNavigationCommands() {
const navigator = useService(Navigator);
useEffect(() => {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:shortcut-history-go-back',
category: 'affine:general',
preconditionStrategy: PreconditionStrategy.Never,
icon: 'none',
label: 'go back',
keyBinding: {
binding: '$mod+[',
},
run() {
navigator.back();
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:shortcut-history-go-forward',
category: 'affine:general',
preconditionStrategy: PreconditionStrategy.Never,
icon: 'none',
label: 'go forward',
keyBinding: {
binding: '$mod+]',
},
run() {
navigator.forward();
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}, [navigator]);
}

View File

@@ -11,6 +11,7 @@ import {
LocalStorageGlobalCache,
LocalStorageGlobalState,
} from './infra-web/storage';
import { Navigator } from './navigation';
import { RightSidebar } from './right-sidebar/entities/right-sidebar';
import { Workbench } from './workbench';
import {
@@ -24,6 +25,7 @@ export function configureBusinessServices(services: ServiceCollection) {
services
.scope(WorkspaceScope)
.add(Workbench)
.add(Navigator, [Workbench])
.add(RightSidebar)
.add(WorkspacePropertiesAdapter, [Workspace])
.add(CollectionService, [Workspace])

View File

@@ -1,15 +1,25 @@
import { LiveData } from '@toeverything/infra';
import type { Location, To } from 'history';
import { createMemoryHistory } from 'history';
import { nanoid } from 'nanoid';
import { Observable } from 'rxjs';
import { createIsland } from '../../../utils/island';
import { createNavigableHistory } from '../../../utils/navigable-history';
export class View {
constructor(defaultPath: To = { pathname: '/all' }) {
this.history = createNavigableHistory({
initialEntries: [defaultPath],
initialIndex: 0,
});
}
id = nanoid();
history = createMemoryHistory();
history = createNavigableHistory({
initialEntries: ['/all'],
initialIndex: 0,
});
location = LiveData.from<Location>(
new Observable(subscriber => {
@@ -20,6 +30,17 @@ export class View {
}),
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);
header = createIsland();

View File

@@ -27,8 +27,8 @@ export class Workbench {
this.activeViewIndex.next(index);
}
createView(at: WorkbenchPosition = 'beside') {
const view = new View();
createView(at: WorkbenchPosition = 'beside', defaultLocation: To) {
const view = new View(defaultLocation);
const newViews = [...this.views.value];
newViews.splice(this.indexAt(at), 0, view);
this.views.next(newViews);
@@ -44,16 +44,17 @@ export class Workbench {
) {
let view = this.viewAt(at);
if (!view) {
const newIndex = this.createView(at);
const newIndex = this.createView(at, to);
view = this.viewAt(newIndex);
if (!view) {
throw new Unreachable();
}
}
if (replaceHistory) {
view.history.replace(to);
} else {
view.history.push(to);
if (replaceHistory) {
view.history.replace(to);
} else {
view.history.push(to);
}
}
}

View File

@@ -30,6 +30,13 @@ export function useBindWorkbenchToDesktopRouter(
if (newLocation === null) {
return;
}
if (
workbench.location.value.pathname === newLocation.pathname &&
workbench.location.value.search === newLocation.search &&
workbench.location.value.hash === newLocation.hash
) {
return;
}
workbench.open(newLocation);
}, [basename, browserLocation, workbench]);

View File

@@ -1,8 +1,4 @@
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 { RightSidebarIcon } from '@blocksuite/icons';
import { useLiveData } from '@toeverything/infra';
@@ -10,6 +6,10 @@ import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai';
import { Suspense, useCallback } from 'react';
import {
appSidebarOpenAtom,
SidebarSwitch,
} from '../../../components/app-sidebar';
import { RightSidebar } from '../../right-sidebar';
import * as styles from './route-container.css';
import { useView } from './use-view';