mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 06:18:45 +08:00
feat(core): events list in journal calendar (#11873)
close AF-2505 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced calendar event integration in the journal detail page, allowing users to view and interact with calendar events for a selected date. - Added the ability to create new linked documents from calendar events directly within the journal interface. - **Improvements** - Enhanced calendar integration with support for all-day and timed events, improved event grouping by date, and real-time updates. - Added new English localization entries for calendar event labels. - **Bug Fixes** - Improved URL handling for calendar subscriptions, ensuring better compatibility and error feedback. - **Style** - Added new styles for calendar event components to ensure a consistent and user-friendly appearance. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+71
@@ -0,0 +1,71 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
|
||||
export const primaryColor = createVar('calendar-event-primary');
|
||||
|
||||
export const list = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
padding: '0px 16px 10px 16px',
|
||||
});
|
||||
|
||||
export const event = style({
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
padding: '5px 4px',
|
||||
borderRadius: 4,
|
||||
cursor: 'pointer',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
backgroundColor: cssVarV2.layer.background.hoverOverlay,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const eventIcon = style({
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: primaryColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 20,
|
||||
});
|
||||
|
||||
export const eventTitle = style({
|
||||
width: 0,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
lineHeight: '22px',
|
||||
color: cssVarV2.text.primary,
|
||||
});
|
||||
|
||||
export const eventCaption = style({
|
||||
fontSize: 12,
|
||||
lineHeight: '20px',
|
||||
color: cssVarV2.text.secondary,
|
||||
});
|
||||
|
||||
export const eventTime = style({
|
||||
display: 'flex',
|
||||
selectors: {
|
||||
[`${event}:hover &`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const eventNewDoc = style({
|
||||
display: 'none',
|
||||
gap: 4,
|
||||
selectors: {
|
||||
[`${event}:hover &`]: {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
});
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { Loading, toast } from '@affine/component';
|
||||
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import {
|
||||
type CalendarEvent,
|
||||
IntegrationService,
|
||||
} from '@affine/core/modules/integration';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { FullDayIcon, PeriodIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import type ICAL from 'ical.js';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import * as styles from './calendar-events.css';
|
||||
|
||||
const pad = (val?: number) => (val ?? 0).toString().padStart(2, '0');
|
||||
|
||||
function formatTime(start?: ICAL.Time, end?: ICAL.Time) {
|
||||
const from = `${pad(start?.hour)}:${pad(start?.minute)}`;
|
||||
const to = `${pad(end?.hour)}:${pad(end?.minute)}`;
|
||||
return from === to ? from : `${from} - ${to}`;
|
||||
}
|
||||
|
||||
export const CalendarEvents = ({ date }: { date: Dayjs }) => {
|
||||
const calendar = useService(IntegrationService).calendar;
|
||||
const events = useLiveData(
|
||||
useMemo(() => calendar.eventsByDate$(date), [calendar, date])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
calendar.subscriptions$.value.forEach(sub => sub.update());
|
||||
};
|
||||
update();
|
||||
const interval = setInterval(update, 5 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [calendar]);
|
||||
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
{events.map(event => (
|
||||
<CalendarEventRenderer key={event.id} event={event} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const CalendarEventRenderer = ({ event }: { event: CalendarEvent }) => {
|
||||
const t = useI18n();
|
||||
const { url, title, startAt, endAt, allDay, date } = event;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const calendar = useService(IntegrationService).calendar;
|
||||
const docsService = useService(DocsService);
|
||||
const guardService = useService(GuardService);
|
||||
const journalService = useService(JournalService);
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const { createPage } = usePageHelper(
|
||||
workspaceService.workspace.docCollection
|
||||
);
|
||||
const subscription = useLiveData(
|
||||
useMemo(() => calendar.subscription$(url), [calendar, url])
|
||||
);
|
||||
const config = useLiveData(
|
||||
useMemo(() => subscription?.config$, [subscription?.config$])
|
||||
);
|
||||
const color = config?.color || cssVarV2.button.primary;
|
||||
|
||||
const handleClick = useAsyncCallback(async () => {
|
||||
if (!date || loading) return;
|
||||
const docs = journalService.journalsByDate$(
|
||||
date.format('YYYY-MM-DD')
|
||||
).value;
|
||||
if (docs.length === 0) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
for (const doc of docs) {
|
||||
const canEdit = await guardService.can('Doc_Update', doc.id);
|
||||
if (!canEdit) {
|
||||
toast(t['com.affine.no-permission']());
|
||||
continue;
|
||||
}
|
||||
|
||||
const newDoc = createPage();
|
||||
await docsService.changeDocTitle(newDoc.id, title);
|
||||
await docsService.addLinkedDoc(doc.id, newDoc.id);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
createPage,
|
||||
date,
|
||||
docsService,
|
||||
guardService,
|
||||
journalService,
|
||||
loading,
|
||||
t,
|
||||
title,
|
||||
]);
|
||||
|
||||
return (
|
||||
<li
|
||||
style={assignInlineVars({
|
||||
[styles.primaryColor]: color,
|
||||
})}
|
||||
className={styles.event}
|
||||
data-all-day={allDay}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={styles.eventIcon}>
|
||||
{allDay ? <FullDayIcon /> : <PeriodIcon />}
|
||||
</div>
|
||||
<div className={styles.eventTitle}>{title}</div>
|
||||
{loading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<div className={styles.eventCaption}>
|
||||
<span className={styles.eventTime}>
|
||||
{allDay
|
||||
? t['com.affine.integration.calendar.all-day']()
|
||||
: formatTime(startAt, endAt)}
|
||||
</span>
|
||||
<span className={styles.eventNewDoc}>
|
||||
<PlusIcon style={{ fontSize: 18 }} />
|
||||
{t['com.affine.integration.calendar.new-doc']()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
@@ -32,6 +32,7 @@ import dayjs from 'dayjs';
|
||||
import type { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { CalendarEvents } from './calendar-events';
|
||||
import * as styles from './journal.css';
|
||||
import { JournalTemplateOnboarding } from './template-onboarding';
|
||||
import { JournalTemplateSetting } from './template-setting';
|
||||
@@ -166,6 +167,7 @@ export const EditorJournalPanel = () => {
|
||||
{journalDate ? (
|
||||
<>
|
||||
<JournalConflictBlock date={journalDate} />
|
||||
<CalendarEvents date={journalDate} />
|
||||
<JournalDailyCountBlock date={journalDate} />
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user