mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 02:21:51 +08:00
feat(core): journal sidebar conflict block (#5574)
This commit is contained in:
+49
-1
@@ -1,7 +1,8 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
const interactive = style({
|
const interactive = style({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
cursor: 'pointer',
|
||||||
|
|
||||||
selectors: {
|
selectors: {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@@ -134,6 +135,12 @@ export const pageItem = style([
|
|||||||
padding: '0 4px',
|
padding: '0 4px',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
height: 30,
|
height: 30,
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&[aria-selected="true"]': {
|
||||||
|
backgroundColor: 'var(--affine-hover-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
export const pageItemIcon = style({
|
export const pageItemIcon = style({
|
||||||
@@ -151,4 +158,45 @@ export const pageItemLabel = style({
|
|||||||
fontSize: 'var(--affine-font-size-sm)',
|
fontSize: 'var(--affine-font-size-sm)',
|
||||||
color: 'var(--affine-text-primary-color)',
|
color: 'var(--affine-text-primary-color)',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
|
selectors: {
|
||||||
|
'[aria-selected="true"] &': {
|
||||||
|
// TODO: wait for design
|
||||||
|
color: 'var(--affine-primary-color)',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// conflict
|
||||||
|
export const journalConflictBlock = style({
|
||||||
|
padding: '0 16px 16px 16px',
|
||||||
|
});
|
||||||
|
export const journalConflictWrapper = style({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(170px, 1fr))',
|
||||||
|
rowGap: 4,
|
||||||
|
columnGap: 8,
|
||||||
|
});
|
||||||
|
export const journalConflictMoreTrigger = style([
|
||||||
|
interactive,
|
||||||
|
{
|
||||||
|
color: 'var(--affine-text-secondary-color)',
|
||||||
|
height: 30,
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: '0px 8px',
|
||||||
|
fontSize: 'var(--affine-font-size-sm)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO: when date-picker's cell is customizable, we should implement by custom cell
|
||||||
|
// override date-picker's active day when is not journal
|
||||||
|
globalStyle(
|
||||||
|
`.${journalPanel}[data-is-journal="false"] .react-datepicker__day[aria-selected="true"]`,
|
||||||
|
{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: 'var(--affine-text-primary-color)',
|
||||||
|
fontWeight: 500,
|
||||||
|
border: '1px solid var(--affine-primary-color)',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
+126
-25
@@ -1,16 +1,28 @@
|
|||||||
import { AFFiNEDatePicker, Scrollable } from '@affine/component';
|
import {
|
||||||
|
AFFiNEDatePicker,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
Scrollable,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||||
|
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
|
||||||
import {
|
import {
|
||||||
useJournalHelper,
|
useJournalHelper,
|
||||||
useJournalInfoHelper,
|
useJournalInfoHelper,
|
||||||
} from '@affine/core/hooks/use-journal';
|
} from '@affine/core/hooks/use-journal';
|
||||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { EdgelessIcon, PageIcon, TodayIcon } from '@blocksuite/icons';
|
import {
|
||||||
|
EdgelessIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
PageIcon,
|
||||||
|
TodayIcon,
|
||||||
|
} from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import type { HTMLAttributes, ReactNode } from 'react';
|
import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type { EditorExtension, EditorExtensionProps } from '..';
|
import type { EditorExtension, EditorExtensionProps } from '..';
|
||||||
@@ -26,30 +38,30 @@ const CountDisplay = ({
|
|||||||
}: { count: number; max?: number } & HTMLAttributes<HTMLSpanElement>) => {
|
}: { count: number; max?: number } & HTMLAttributes<HTMLSpanElement>) => {
|
||||||
return <span {...attrs}>{count > max ? `${max}+` : count}</span>;
|
return <span {...attrs}>{count > max ? `${max}+` : count}</span>;
|
||||||
};
|
};
|
||||||
interface PageItemProps extends HTMLAttributes<HTMLButtonElement> {
|
interface PageItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
page: Page;
|
page: Page;
|
||||||
right?: ReactNode;
|
right?: ReactNode;
|
||||||
}
|
}
|
||||||
const PageItem = ({ page, right, className, ...attrs }: PageItemProps) => {
|
const PageItem = ({ page, right, className, ...attrs }: PageItemProps) => {
|
||||||
const { isJournal } = useJournalInfoHelper(page.meta);
|
const { isJournal } = useJournalInfoHelper(page.meta);
|
||||||
|
|
||||||
const icon = isJournal ? (
|
const Icon = isJournal
|
||||||
<TodayIcon width={20} height={20} />
|
? TodayIcon
|
||||||
) : page.meta.mode === 'edgeless' ? (
|
: page.meta.mode === 'edgeless'
|
||||||
<EdgelessIcon width={20} height={20} />
|
? EdgelessIcon
|
||||||
) : (
|
: PageIcon;
|
||||||
<PageIcon width={20} height={20} />
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
aria-label={page.meta.title}
|
aria-label={page.meta.title}
|
||||||
className={clsx(className, styles.pageItem)}
|
className={clsx(className, styles.pageItem)}
|
||||||
{...attrs}
|
{...attrs}
|
||||||
>
|
>
|
||||||
<div className={styles.pageItemIcon}>{icon}</div>
|
<div className={styles.pageItemIcon}>
|
||||||
|
<Icon width={20} height={20} />
|
||||||
|
</div>
|
||||||
<span className={styles.pageItemLabel}>{page.meta.title}</span>
|
<span className={styles.pageItemLabel}>{page.meta.title}</span>
|
||||||
{right}
|
{right}
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,10 +71,13 @@ interface NavItem {
|
|||||||
label: string;
|
label: string;
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
interface JournalBlockProps extends EditorExtensionProps {
|
||||||
|
date: dayjs.Dayjs;
|
||||||
|
}
|
||||||
|
|
||||||
const EditorJournalPanel = (props: EditorExtensionProps) => {
|
const EditorJournalPanel = (props: EditorExtensionProps) => {
|
||||||
const { workspace, page } = props;
|
const { workspace, page } = props;
|
||||||
const { journalDate } = useJournalInfoHelper(page?.meta);
|
const { journalDate, isJournal } = useJournalInfoHelper(page?.meta);
|
||||||
const { openJournal } = useJournalHelper(workspace);
|
const { openJournal } = useJournalHelper(workspace);
|
||||||
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
@@ -79,15 +94,15 @@ const EditorJournalPanel = (props: EditorExtensionProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.journalPanel}>
|
<div className={styles.journalPanel} data-is-journal={isJournal}>
|
||||||
<AFFiNEDatePicker
|
<AFFiNEDatePicker
|
||||||
inline
|
inline
|
||||||
value={date}
|
value={date}
|
||||||
onSelect={onDateSelect}
|
onSelect={onDateSelect}
|
||||||
calendarClassName={styles.calendar}
|
calendarClassName={styles.calendar}
|
||||||
/>
|
/>
|
||||||
|
<JournalConflictBlock date={dayjs(date)} {...props} />
|
||||||
<JournalDailyCountBlock {...props} />
|
<JournalDailyCountBlock date={dayjs(date)} {...props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -116,10 +131,9 @@ const DailyCountEmptyFallback = ({ name }: { name: NavItemName }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const JournalDailyCountBlock = ({ workspace, page }: EditorExtensionProps) => {
|
const JournalDailyCountBlock = ({ workspace, date }: JournalBlockProps) => {
|
||||||
const nodeRef = useRef<HTMLDivElement>(null);
|
const nodeRef = useRef<HTMLDivElement>(null);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const { journalDate } = useJournalInfoHelper(page?.meta);
|
|
||||||
const [activeItem, setActiveItem] = useState<NavItemName>('createdToday');
|
const [activeItem, setActiveItem] = useState<NavItemName>('createdToday');
|
||||||
|
|
||||||
const navigateHelper = useNavigateHelper();
|
const navigateHelper = useNavigateHelper();
|
||||||
@@ -129,16 +143,13 @@ const JournalDailyCountBlock = ({ workspace, page }: EditorExtensionProps) => {
|
|||||||
const pages: Page[] = [];
|
const pages: Page[] = [];
|
||||||
Array.from(workspace.pages.values()).forEach(page => {
|
Array.from(workspace.pages.values()).forEach(page => {
|
||||||
if (page.meta.trash) return;
|
if (page.meta.trash) return;
|
||||||
if (
|
if (page.meta[field] && dayjs(page.meta[field]).isSame(date, 'day')) {
|
||||||
page.meta[field] &&
|
|
||||||
dayjs(page.meta[field]).isSame(journalDate, 'day')
|
|
||||||
) {
|
|
||||||
pages.push(page);
|
pages.push(page);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return sortPagesByDate(pages, field);
|
return sortPagesByDate(pages, field);
|
||||||
},
|
},
|
||||||
[journalDate, workspace.pages]
|
[date, workspace.pages]
|
||||||
);
|
);
|
||||||
|
|
||||||
const createdToday = useMemo(
|
const createdToday = useMemo(
|
||||||
@@ -227,6 +238,96 @@ const JournalDailyCountBlock = ({ workspace, page }: EditorExtensionProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_CONFLICT_COUNT = 5;
|
||||||
|
interface ConflictListProps
|
||||||
|
extends JournalBlockProps,
|
||||||
|
PropsWithChildren,
|
||||||
|
HTMLAttributes<HTMLDivElement> {
|
||||||
|
pages: Page[];
|
||||||
|
}
|
||||||
|
const ConflictList = ({
|
||||||
|
page: currentPage,
|
||||||
|
pages,
|
||||||
|
workspace,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...attrs
|
||||||
|
}: ConflictListProps) => {
|
||||||
|
const navigateHelper = useNavigateHelper();
|
||||||
|
const { setTrashModal } = useTrashModalHelper(workspace);
|
||||||
|
|
||||||
|
const handleOpenTrashModal = useCallback(
|
||||||
|
(page: Page) => {
|
||||||
|
if (!page.meta) return;
|
||||||
|
setTrashModal({
|
||||||
|
open: true,
|
||||||
|
pageIds: [page.id],
|
||||||
|
pageTitles: [page.meta.title],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setTrashModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(styles.journalConflictWrapper, className)} {...attrs}>
|
||||||
|
{pages.map(page => {
|
||||||
|
const isCurrent = page.id === currentPage.id;
|
||||||
|
return (
|
||||||
|
<PageItem
|
||||||
|
aria-label={page.meta.title}
|
||||||
|
aria-selected={isCurrent}
|
||||||
|
page={page}
|
||||||
|
key={page.id}
|
||||||
|
right={
|
||||||
|
<Menu
|
||||||
|
items={
|
||||||
|
<MoveToTrash onSelect={() => handleOpenTrashModal(page)} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton type="plain">
|
||||||
|
<MoreHorizontalIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
onClick={() => navigateHelper.openPage(workspace.id, page.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const JournalConflictBlock = (props: JournalBlockProps) => {
|
||||||
|
const { workspace, date } = props;
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const journalHelper = useJournalHelper(workspace);
|
||||||
|
const pages = journalHelper.getJournalsByDate(date.format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
if (pages.length <= 1) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConflictList
|
||||||
|
className={styles.journalConflictBlock}
|
||||||
|
pages={pages.slice(0, MAX_CONFLICT_COUNT)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{pages.length > MAX_CONFLICT_COUNT ? (
|
||||||
|
<Menu
|
||||||
|
items={
|
||||||
|
<ConflictList pages={pages.slice(MAX_CONFLICT_COUNT)} {...props} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.journalConflictMoreTrigger}>
|
||||||
|
{t['com.affine.journal.conflict-show-more']({
|
||||||
|
count: (pages.length - MAX_CONFLICT_COUNT).toFixed(0),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
) : null}
|
||||||
|
</ConflictList>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const journalExtension: EditorExtension = {
|
export const journalExtension: EditorExtension = {
|
||||||
name: 'journal',
|
name: 'journal',
|
||||||
icon: <TodayIcon />,
|
icon: <TodayIcon />,
|
||||||
|
|||||||
@@ -1047,5 +1047,6 @@
|
|||||||
"com.affine.journal.created-today": "Created today",
|
"com.affine.journal.created-today": "Created today",
|
||||||
"com.affine.journal.updated-today": "Updated today",
|
"com.affine.journal.updated-today": "Updated today",
|
||||||
"com.affine.journal.daily-count-created-empty-tips": "You haven't created anything yet",
|
"com.affine.journal.daily-count-created-empty-tips": "You haven't created anything yet",
|
||||||
"com.affine.journal.daily-count-updated-empty-tips": "You haven't updated anything yet"
|
"com.affine.journal.daily-count-updated-empty-tips": "You haven't updated anything yet",
|
||||||
|
"com.affine.journal.conflict-show-more": "{{count}} more articles"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user