mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(core): new "is journal" page property (#8525)
close AF-1450, AF-1451, AF-14552
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||
import { useInsidePeekView } from '@affine/core/modules/peek-view/view/modal-container';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
@@ -30,7 +30,8 @@ export function AffinePageReference({
|
||||
params?: URLSearchParams;
|
||||
}) {
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const journalHelper = useJournalInfoHelper();
|
||||
const journalService = useService(JournalService);
|
||||
const isJournal = !!useLiveData(journalService.journalDate$(pageId));
|
||||
const i18n = useI18n();
|
||||
|
||||
let linkWithMode: DocMode | null = null;
|
||||
@@ -67,7 +68,6 @@ export function AffinePageReference({
|
||||
|
||||
const peekView = useService(PeekViewService).peekView;
|
||||
const isInPeekView = useInsidePeekView();
|
||||
const isJournal = journalHelper.isPageJournal(pageId);
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
@@ -127,7 +127,8 @@ export function AffineSharedPageReference({
|
||||
params?: URLSearchParams;
|
||||
}) {
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const journalHelper = useJournalInfoHelper();
|
||||
const journalService = useService(JournalService);
|
||||
const isJournal = !!useLiveData(journalService.journalDate$(pageId));
|
||||
const i18n = useI18n();
|
||||
|
||||
let linkWithMode: DocMode | null = null;
|
||||
@@ -159,8 +160,6 @@ export function AffineSharedPageReference({
|
||||
|
||||
const [refreshKey, setRefreshKey] = useState<string>(() => nanoid());
|
||||
|
||||
const isJournal = journalHelper.isPageJournal(pageId);
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (isJournal) {
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { i18nTime, useI18n } from '@affine/i18n';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const BlocksuiteEditorJournalDocTitle = ({ page }: { page: Doc }) => {
|
||||
const { localizedJournalDate, isTodayJournal, journalDate } =
|
||||
useJournalInfoHelper(page.id);
|
||||
const journalService = useService(JournalService);
|
||||
const journalDateStr = useLiveData(journalService.journalDate$(page.id));
|
||||
const journalDate = journalDateStr ? dayjs(journalDateStr) : null;
|
||||
const isTodayJournal = useLiveData(journalService.journalToday$(page.id));
|
||||
const localizedJournalDate = i18nTime(journalDateStr, {
|
||||
absolute: { accuracy: 'day' },
|
||||
});
|
||||
const t = useI18n();
|
||||
|
||||
// TODO(catsjuice): i18n
|
||||
const day = journalDate?.format('dddd') ?? null;
|
||||
|
||||
return (
|
||||
<span className="doc-title-container">
|
||||
<span>{localizedJournalDate}</span>
|
||||
<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.titleDayTag}>{day}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@ import {
|
||||
useConfirmModal,
|
||||
useLitPortalFactory,
|
||||
} from '@affine/component';
|
||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { ServerConfigService } from '@affine/core/modules/cloud';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-settting';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
@@ -196,7 +196,8 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
) {
|
||||
const titleRef = useRef<DocTitle | null>(null);
|
||||
const docRef = useRef<PageEditor | null>(null);
|
||||
const { isJournal } = useJournalInfoHelper(page.id);
|
||||
const journalService = useService(JournalService);
|
||||
const isJournal = !!useLiveData(journalService.journalDate$(page.id));
|
||||
|
||||
const editorSettingService = useService(EditorSettingService);
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ const titleTagBasic = style({
|
||||
padding: '0 4px',
|
||||
borderRadius: '4px',
|
||||
marginLeft: '4px',
|
||||
lineHeight: '0px',
|
||||
});
|
||||
export const titleDayTag = style([
|
||||
titleTagBasic,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { WeekDatePickerHandle } from '@affine/component';
|
||||
import { WeekDatePicker } from '@affine/component';
|
||||
import {
|
||||
useJournalInfoHelper,
|
||||
useJournalRouteHelper,
|
||||
} from '@affine/core/components/hooks/use-journal';
|
||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import type { Doc, DocCollection } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
@@ -19,7 +18,9 @@ export const JournalWeekDatePicker = ({
|
||||
page,
|
||||
}: JournalWeekDatePickerProps) => {
|
||||
const handleRef = useRef<WeekDatePickerHandle>(null);
|
||||
const { journalDate } = useJournalInfoHelper(page.id);
|
||||
const journalService = useService(JournalService);
|
||||
const journalDateStr = useLiveData(journalService.journalDate$(page.id));
|
||||
const journalDate = journalDateStr ? dayjs(journalDateStr) : null;
|
||||
const { openJournal } = useJournalRouteHelper(docCollection);
|
||||
const [date, setDate] = useState(
|
||||
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
||||
@@ -33,6 +34,7 @@ export const JournalWeekDatePicker = ({
|
||||
|
||||
return (
|
||||
<WeekDatePicker
|
||||
data-testid="journal-week-picker"
|
||||
handleRef={handleRef}
|
||||
style={weekStyle}
|
||||
value={date}
|
||||
|
||||
@@ -318,6 +318,7 @@ export const DocPropertyRow = ({
|
||||
hideEmpty={hideEmpty}
|
||||
hide={hide}
|
||||
data-testid="doc-property-row"
|
||||
data-info-id={propertyInfo.id}
|
||||
>
|
||||
<PropertyName
|
||||
defaultOpenMenu={defaultOpenEditMenu}
|
||||
@@ -414,6 +415,7 @@ export const DocPropertiesTableBody = forwardRef<
|
||||
variant="plain"
|
||||
prefix={<PlusIcon />}
|
||||
className={styles.propertyActionButton}
|
||||
data-testid="add-property-button"
|
||||
>
|
||||
{t['com.affine.page-properties.add-property']()}
|
||||
</Button>
|
||||
|
||||
@@ -7,12 +7,14 @@ import {
|
||||
NumberIcon,
|
||||
TagIcon,
|
||||
TextIcon,
|
||||
TodayIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
|
||||
import { CheckboxValue } from './checkbox';
|
||||
import { CreatedByValue, UpdatedByValue } from './created-updated-by';
|
||||
import { DateValue } from './date';
|
||||
import { DocPrimaryModeValue } from './doc-primary-mode';
|
||||
import { JournalValue } from './journal';
|
||||
import { NumberValue } from './number';
|
||||
import { TagsValue } from './tags';
|
||||
import { TextValue } from './text';
|
||||
@@ -61,6 +63,13 @@ export const DocPropertyTypes = {
|
||||
value: DocPrimaryModeValue,
|
||||
name: 'com.affine.page-properties.property.docPrimaryMode',
|
||||
},
|
||||
journal: {
|
||||
icon: TodayIcon,
|
||||
value: JournalValue,
|
||||
name: 'com.affine.page-properties.property.journal',
|
||||
uniqueId: 'journal',
|
||||
renameable: false,
|
||||
},
|
||||
} as Record<
|
||||
string,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const property = style({
|
||||
padding: 4,
|
||||
});
|
||||
|
||||
export const root = style({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const checkbox = style({
|
||||
fontSize: 24,
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
|
||||
export const date = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVarV2('text/primary'),
|
||||
lineHeight: '22px',
|
||||
padding: '0 4px',
|
||||
borderRadius: 4,
|
||||
':hover': {
|
||||
background: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
});
|
||||
|
||||
export const duplicateTag = style({
|
||||
padding: '0 8px',
|
||||
border: `1px solid ${cssVarV2('database/border')}`,
|
||||
background: cssVarV2('layer/background/error'),
|
||||
color: cssVarV2('toast/iconState/error'),
|
||||
borderRadius: 4,
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Checkbox, DatePicker, Menu, PropertyValue } from '@affine/component';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import { i18nTime, useI18n } from '@affine/i18n';
|
||||
import {
|
||||
DocService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
} from '@toeverything/infra';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import * as styles from './journal.css';
|
||||
|
||||
export const JournalValue = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const journalService = useService(JournalService);
|
||||
const doc = useService(DocService).doc;
|
||||
const journalDate = useLiveData(journalService.journalDate$(doc.id));
|
||||
const checked = !!journalDate;
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState(
|
||||
dayjs().format('YYYY-MM-DD')
|
||||
);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const displayDate = useMemo(
|
||||
() =>
|
||||
i18nTime(selectedDate, {
|
||||
absolute: { accuracy: 'day' },
|
||||
}),
|
||||
[selectedDate]
|
||||
);
|
||||
const docs = useLiveData(
|
||||
useMemo(
|
||||
() => journalService.journalsByDate$(selectedDate),
|
||||
[journalService, selectedDate]
|
||||
)
|
||||
);
|
||||
const conflict = docs.length > 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (journalDate) setSelectedDate(journalDate);
|
||||
}, [journalDate]);
|
||||
|
||||
const handleDateSelect = useCallback(
|
||||
(day: string) => {
|
||||
const date = dayjs(day).format('YYYY-MM-DD');
|
||||
setSelectedDate(date);
|
||||
journalService.setJournalDate(doc.id, date);
|
||||
},
|
||||
[journalService, doc.id]
|
||||
);
|
||||
|
||||
const handleCheck = useCallback(
|
||||
(_: unknown, v: boolean) => {
|
||||
if (!v) {
|
||||
journalService.removeJournalDate(doc.id);
|
||||
} else {
|
||||
handleDateSelect(selectedDate);
|
||||
}
|
||||
},
|
||||
[handleDateSelect, journalService, doc.id, selectedDate]
|
||||
);
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const activeView = useLiveData(workbench.activeView$);
|
||||
const view = useServiceOptional(ViewService)?.view ?? activeView;
|
||||
|
||||
const handleOpenDuplicate = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
workbench.openSidebar();
|
||||
view.activeSidebarTab('journal');
|
||||
},
|
||||
[view, workbench]
|
||||
);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
handleCheck(null, !checked);
|
||||
}, [checked, handleCheck]);
|
||||
|
||||
return (
|
||||
<PropertyValue className={styles.property} onClick={toggle}>
|
||||
<div className={styles.root}>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
checked={checked}
|
||||
onChange={handleCheck}
|
||||
/>
|
||||
{checked ? (
|
||||
<Menu
|
||||
contentOptions={{
|
||||
onClick: e => e.stopPropagation(),
|
||||
sideOffset: 10,
|
||||
alignOffset: -30,
|
||||
style: { padding: 20 },
|
||||
}}
|
||||
rootOptions={{
|
||||
modal: true,
|
||||
open: showDatePicker,
|
||||
onOpenChange: setShowDatePicker,
|
||||
}}
|
||||
items={
|
||||
<DatePicker
|
||||
weekDays={t['com.affine.calendar-date-picker.week-days']()}
|
||||
monthNames={t['com.affine.calendar-date-picker.month-names']()}
|
||||
todayLabel={t['com.affine.calendar-date-picker.today']()}
|
||||
value={selectedDate}
|
||||
onChange={handleDateSelect}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div data-testid="date-selector" className={styles.date}>
|
||||
{displayDate}
|
||||
</div>
|
||||
</Menu>
|
||||
) : null}
|
||||
|
||||
{checked && conflict ? (
|
||||
<div
|
||||
data-testid="conflict-tag"
|
||||
className={styles.duplicateTag}
|
||||
onClick={handleOpenDuplicate}
|
||||
>
|
||||
{t['com.affine.page-properties.property.journal-duplicated']()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</PropertyValue>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user