feat(core): drop doc onto split view (#9487)

fix AF-2068, AF-2069, AF-1175, AF-2061, AF-2079, AF-2034, AF-2080, AF-1960, AF-2081

1. replace `dnd-kit` with `@atlaskit/pragmatic-drag-and-drop`
2. allow creating split views by drag & drop the following
   a. WorkbenchLinks (route links), like journals, trash, all docs
   b. doc refs
   c. tags/collection
3. style adjustments to split view
4. remove split view's feature flag and make it GA for electron

https://github.com/user-attachments/assets/6a3e4a25-faa2-4215-8eb0-983f44db6e8c
This commit is contained in:
pengx17
2025-01-08 05:05:33 +00:00
parent c0ed74dfed
commit a4841bbfa3
53 changed files with 1574 additions and 905 deletions

View File

@@ -23,7 +23,9 @@ export const BlocksuiteEditorJournalDocTitle = ({ page }: { page: Blocks }) => {
<div className="doc-title-container" data-testid="journal-title">
<span data-testid="date">{localizedJournalDate}</span>
{isTodayJournal ? (
<span className={styles.titleTodayTag}>{t['com.affine.today']()}</span>
<span className={styles.titleTodayTag} data-testid="date-today-label">
{t['com.affine.today']()}
</span>
) : (
<span className={styles.titleDayTag}>{day}</span>
)}

View File

@@ -8,7 +8,9 @@ export const JournalTodayButton = () => {
const journalHelper = useJournalRouteHelper();
const onToday = useCallback(() => {
journalHelper.openToday();
journalHelper.openToday({
replaceHistory: true,
});
}, [journalHelper]);
return (

View File

@@ -5,6 +5,7 @@ import {
JournalService,
type MaybeDate,
} from '@affine/core/modules/journal';
import type { WorkbenchOpenOptions } from '@affine/core/modules/workbench/entities/workbench';
import { i18nTime } from '@affine/i18n';
import { track } from '@affine/track';
import { useService, useServices } from '@toeverything/infra';
@@ -60,9 +61,9 @@ export const useJournalRouteHelper = () => {
* open journal by date, create one if not exist
*/
const openJournal = useCallback(
(maybeDate: MaybeDate, newTab?: boolean) => {
(maybeDate: MaybeDate, options?: WorkbenchOpenOptions) => {
const page = getJournalByDate(maybeDate);
workbench.openDoc(page.id, { at: newTab ? 'new-tab' : 'active' });
workbench.openDoc(page.id, options);
track.$.navigationPanel.journal.navigate({
to: 'journal',
});
@@ -75,9 +76,9 @@ export const useJournalRouteHelper = () => {
* open today's journal
*/
const openToday = useCallback(
(newTab?: boolean) => {
(options: WorkbenchOpenOptions) => {
const date = dayjs().format(JOURNAL_DATE_FORMAT);
return openJournal(date, newTab);
return openJournal(date, options);
},
[openJournal]
);

View File

@@ -180,7 +180,8 @@ export const PageListItem = (props: PageListItemProps) => {
},
},
}),
[props.draggable, props.pageId]
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.draggable, props.pageId, props.selectable]
);
return (

View File

@@ -14,7 +14,6 @@ import {
CompatibleFavoriteItemsAdapter,
FavoriteService,
} from '@affine/core/modules/favorite';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
@@ -65,19 +64,15 @@ export const PageOperationCell = ({
}: PageOperationCellProps) => {
const t = useI18n();
const {
featureFlagService,
workspaceService,
compatibleFavoriteItemsAdapter: favAdapter,
workbenchService,
} = useServices({
FeatureFlagService,
WorkspaceService,
CompatibleFavoriteItemsAdapter,
WorkbenchService,
});
const enableSplitView = useLiveData(
featureFlagService.flags.enable_multi_view.$
);
const currentWorkspace = workspaceService.workspace;
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
const workbench = workbenchService.workbench;
@@ -194,7 +189,7 @@ export const PageOperationCell = ({
<MenuItem onClick={onOpenInNewTab} prefixIcon={<OpenInNewIcon />}>
{t['com.affine.workbench.tab.page-menu-open']()}
</MenuItem>
{BUILD_CONFIG.isElectron && enableSplitView ? (
{BUILD_CONFIG.isElectron ? (
<MenuItem onClick={onOpenInSplitView} prefixIcon={<SplitViewIcon />}>
{t['com.affine.workbench.split-view.page-menu-open']()}
</MenuItem>

View File

@@ -1,3 +1,4 @@
import { shallowEqual } from '@affine/component';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import type { Tag } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
@@ -34,7 +35,6 @@ import type {
TagListItemProps,
TagMeta,
} from './types';
import { shallowEqual } from './utils';
export const ItemGroupHeader = memo(function ItemGroupHeader<
T extends ListItem,

View File

@@ -1,3 +1,4 @@
import { shallowEqual } from '@affine/component';
import { DEFAULT_SORT_KEY } from '@affine/env/constant';
import { atom } from 'jotai';
import { selectAtom } from 'jotai/utils';
@@ -10,7 +11,6 @@ import type {
MetaRecord,
VirtualizedListProps,
} from './types';
import { shallowEqual } from './utils';
// for ease of use in the component tree
// note: must use selectAtom to access this atom for efficiency

View File

@@ -56,38 +56,3 @@ export const betweenDaysAgo = (
): boolean => {
return !withinDaysAgo(date, days0) && withinDaysAgo(date, days1);
};
// credit: https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/core/shallowEqual.js
export function shallowEqual(objA: any, objB: any) {
if (Object.is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (const key of keysA) {
if (
!Object.prototype.hasOwnProperty.call(objB, key) ||
!Object.is(objA[key], objB[key])
) {
return false;
}
}
return true;
}

View File

@@ -3,7 +3,6 @@ import { Menu, MenuItem, usePromptModal } from '@affine/component';
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WorkbenchService } from '@affine/core/modules/workbench';
import type { Collection } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
@@ -36,21 +35,16 @@ export const CollectionOperations = ({
const {
collectionService: service,
workbenchService,
featureFlagService,
workspaceDialogService,
} = useServices({
CollectionService,
WorkbenchService,
FeatureFlagService,
WorkspaceDialogService,
});
const deleteInfo = useDeleteCollectionInfo();
const workbench = workbenchService.workbench;
const t = useI18n();
const { openPromptModal } = usePromptModal();
const enableMultiView = useLiveData(
featureFlagService.flags.enable_multi_view.$
);
const showEditName = useCallback(() => {
// use openRenameModal if it is in the sidebar collection list
@@ -150,7 +144,7 @@ export const CollectionOperations = ({
name: t['com.affine.workbench.tab.page-menu-open'](),
click: openCollectionNewTab,
},
...(BUILD_CONFIG.isElectron && enableMultiView
...(BUILD_CONFIG.isElectron
? [
{
icon: <SplitViewIcon />,
@@ -172,7 +166,6 @@ export const CollectionOperations = ({
},
],
[
enableMultiView,
t,
showEditName,
showEdit,

View File

@@ -1,32 +1,19 @@
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
import {
useJournalInfoHelper,
useJournalRouteHelper,
} from '@affine/core/components/hooks/use-journal';
import { MenuItem } from '@affine/core/modules/app-sidebar/views';
import { MenuLinkItem } from '@affine/core/modules/app-sidebar/views';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { JournalService } from '@affine/core/modules/journal';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n';
import { TodayIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { type MouseEvent } from 'react';
export const AppSidebarJournalButton = () => {
const t = useI18n();
const docDisplayMetaService = useService(DocDisplayMetaService);
const journalService = useService(JournalService);
const workbench = useService(WorkbenchService).workbench;
const location = useLiveData(workbench.location$);
const { openToday } = useJournalRouteHelper();
const maybeDocId = location.pathname.split('/')[1];
const { isJournal } = useJournalInfoHelper(maybeDocId);
const handleOpenToday = useCatchEventCallback(
(e: MouseEvent) => {
openToday(isNewTabTrigger(e));
},
[openToday]
);
const isJournal = !!useLiveData(journalService.journalDate$(maybeDocId));
const JournalIcon = useLiveData(
docDisplayMetaService.icon$(maybeDocId, {
@@ -36,14 +23,13 @@ export const AppSidebarJournalButton = () => {
const Icon = isJournal ? JournalIcon : TodayIcon;
return (
<MenuItem
<MenuLinkItem
data-testid="slider-bar-journals-button"
active={isJournal}
onClick={handleOpenToday}
onAuxClick={handleOpenToday}
to={'/journals'}
icon={<Icon />}
>
{t['com.affine.journal.app-sidebar-title']()}
</MenuItem>
</MenuLinkItem>
);
};