feat(core): new "is journal" page property (#8525)
close AF-1450, AF-1451, AF-14552
This commit is contained in:
@@ -16,4 +16,9 @@ export const BUILT_IN_CUSTOM_PROPERTY_TYPE = [
|
|||||||
type: 'docPrimaryMode',
|
type: 'docPrimaryMode',
|
||||||
show: 'always-hide',
|
show: 'always-hide',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'journal',
|
||||||
|
type: 'journal',
|
||||||
|
show: 'always-hide',
|
||||||
|
},
|
||||||
] as DocCustomPropertyInfo[];
|
] as DocCustomPropertyInfo[];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
|
||||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
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 { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||||
import { useInsidePeekView } from '@affine/core/modules/peek-view/view/modal-container';
|
import { useInsidePeekView } from '@affine/core/modules/peek-view/view/modal-container';
|
||||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
@@ -30,7 +30,8 @@ export function AffinePageReference({
|
|||||||
params?: URLSearchParams;
|
params?: URLSearchParams;
|
||||||
}) {
|
}) {
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const journalHelper = useJournalInfoHelper();
|
const journalService = useService(JournalService);
|
||||||
|
const isJournal = !!useLiveData(journalService.journalDate$(pageId));
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
let linkWithMode: DocMode | null = null;
|
let linkWithMode: DocMode | null = null;
|
||||||
@@ -67,7 +68,6 @@ export function AffinePageReference({
|
|||||||
|
|
||||||
const peekView = useService(PeekViewService).peekView;
|
const peekView = useService(PeekViewService).peekView;
|
||||||
const isInPeekView = useInsidePeekView();
|
const isInPeekView = useInsidePeekView();
|
||||||
const isJournal = journalHelper.isPageJournal(pageId);
|
|
||||||
|
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -127,7 +127,8 @@ export function AffineSharedPageReference({
|
|||||||
params?: URLSearchParams;
|
params?: URLSearchParams;
|
||||||
}) {
|
}) {
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const journalHelper = useJournalInfoHelper();
|
const journalService = useService(JournalService);
|
||||||
|
const isJournal = !!useLiveData(journalService.journalDate$(pageId));
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
let linkWithMode: DocMode | null = null;
|
let linkWithMode: DocMode | null = null;
|
||||||
@@ -159,8 +160,6 @@ export function AffineSharedPageReference({
|
|||||||
|
|
||||||
const [refreshKey, setRefreshKey] = useState<string>(() => nanoid());
|
const [refreshKey, setRefreshKey] = useState<string>(() => nanoid());
|
||||||
|
|
||||||
const isJournal = journalHelper.isPageJournal(pageId);
|
|
||||||
|
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
if (isJournal) {
|
if (isJournal) {
|
||||||
|
|||||||
@@ -1,25 +1,32 @@
|
|||||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { i18nTime, useI18n } from '@affine/i18n';
|
||||||
import type { Doc } from '@blocksuite/affine/store';
|
import type { Doc } from '@blocksuite/affine/store';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const BlocksuiteEditorJournalDocTitle = ({ page }: { page: Doc }) => {
|
export const BlocksuiteEditorJournalDocTitle = ({ page }: { page: Doc }) => {
|
||||||
const { localizedJournalDate, isTodayJournal, journalDate } =
|
const journalService = useService(JournalService);
|
||||||
useJournalInfoHelper(page.id);
|
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();
|
const t = useI18n();
|
||||||
|
|
||||||
// TODO(catsjuice): i18n
|
// TODO(catsjuice): i18n
|
||||||
const day = journalDate?.format('dddd') ?? null;
|
const day = journalDate?.format('dddd') ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="doc-title-container">
|
<div className="doc-title-container" data-testid="journal-title">
|
||||||
<span>{localizedJournalDate}</span>
|
<span data-testid="date">{localizedJournalDate}</span>
|
||||||
{isTodayJournal ? (
|
{isTodayJournal ? (
|
||||||
<span className={styles.titleTodayTag}>{t['com.affine.today']()}</span>
|
<span className={styles.titleTodayTag}>{t['com.affine.today']()}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.titleDayTag}>{day}</span>
|
<span className={styles.titleDayTag}>{day}</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import {
|
|||||||
useConfirmModal,
|
useConfirmModal,
|
||||||
useLitPortalFactory,
|
useLitPortalFactory,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
|
||||||
import { ServerConfigService } from '@affine/core/modules/cloud';
|
import { ServerConfigService } from '@affine/core/modules/cloud';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
import { EditorSettingService } from '@affine/core/modules/editor-settting';
|
import { EditorSettingService } from '@affine/core/modules/editor-settting';
|
||||||
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||||
@@ -196,7 +196,8 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
) {
|
) {
|
||||||
const titleRef = useRef<DocTitle | null>(null);
|
const titleRef = useRef<DocTitle | null>(null);
|
||||||
const docRef = useRef<PageEditor | 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);
|
const editorSettingService = useService(EditorSettingService);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const titleTagBasic = style({
|
|||||||
padding: '0 4px',
|
padding: '0 4px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
marginLeft: '4px',
|
marginLeft: '4px',
|
||||||
|
lineHeight: '0px',
|
||||||
});
|
});
|
||||||
export const titleDayTag = style([
|
export const titleDayTag = style([
|
||||||
titleTagBasic,
|
titleTagBasic,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { WeekDatePickerHandle } from '@affine/component';
|
import type { WeekDatePickerHandle } from '@affine/component';
|
||||||
import { WeekDatePicker } from '@affine/component';
|
import { WeekDatePicker } from '@affine/component';
|
||||||
import {
|
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||||
useJournalInfoHelper,
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
useJournalRouteHelper,
|
|
||||||
} from '@affine/core/components/hooks/use-journal';
|
|
||||||
import type { Doc, DocCollection } from '@blocksuite/affine/store';
|
import type { Doc, DocCollection } from '@blocksuite/affine/store';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
@@ -19,7 +18,9 @@ export const JournalWeekDatePicker = ({
|
|||||||
page,
|
page,
|
||||||
}: JournalWeekDatePickerProps) => {
|
}: JournalWeekDatePickerProps) => {
|
||||||
const handleRef = useRef<WeekDatePickerHandle>(null);
|
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 { openJournal } = useJournalRouteHelper(docCollection);
|
||||||
const [date, setDate] = useState(
|
const [date, setDate] = useState(
|
||||||
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
||||||
@@ -33,6 +34,7 @@ export const JournalWeekDatePicker = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WeekDatePicker
|
<WeekDatePicker
|
||||||
|
data-testid="journal-week-picker"
|
||||||
handleRef={handleRef}
|
handleRef={handleRef}
|
||||||
style={weekStyle}
|
style={weekStyle}
|
||||||
value={date}
|
value={date}
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ export const DocPropertyRow = ({
|
|||||||
hideEmpty={hideEmpty}
|
hideEmpty={hideEmpty}
|
||||||
hide={hide}
|
hide={hide}
|
||||||
data-testid="doc-property-row"
|
data-testid="doc-property-row"
|
||||||
|
data-info-id={propertyInfo.id}
|
||||||
>
|
>
|
||||||
<PropertyName
|
<PropertyName
|
||||||
defaultOpenMenu={defaultOpenEditMenu}
|
defaultOpenMenu={defaultOpenEditMenu}
|
||||||
@@ -414,6 +415,7 @@ export const DocPropertiesTableBody = forwardRef<
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
prefix={<PlusIcon />}
|
prefix={<PlusIcon />}
|
||||||
className={styles.propertyActionButton}
|
className={styles.propertyActionButton}
|
||||||
|
data-testid="add-property-button"
|
||||||
>
|
>
|
||||||
{t['com.affine.page-properties.add-property']()}
|
{t['com.affine.page-properties.add-property']()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import {
|
|||||||
NumberIcon,
|
NumberIcon,
|
||||||
TagIcon,
|
TagIcon,
|
||||||
TextIcon,
|
TextIcon,
|
||||||
|
TodayIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
import { CheckboxValue } from './checkbox';
|
import { CheckboxValue } from './checkbox';
|
||||||
import { CreatedByValue, UpdatedByValue } from './created-updated-by';
|
import { CreatedByValue, UpdatedByValue } from './created-updated-by';
|
||||||
import { DateValue } from './date';
|
import { DateValue } from './date';
|
||||||
import { DocPrimaryModeValue } from './doc-primary-mode';
|
import { DocPrimaryModeValue } from './doc-primary-mode';
|
||||||
|
import { JournalValue } from './journal';
|
||||||
import { NumberValue } from './number';
|
import { NumberValue } from './number';
|
||||||
import { TagsValue } from './tags';
|
import { TagsValue } from './tags';
|
||||||
import { TextValue } from './text';
|
import { TextValue } from './text';
|
||||||
@@ -61,6 +63,13 @@ export const DocPropertyTypes = {
|
|||||||
value: DocPrimaryModeValue,
|
value: DocPrimaryModeValue,
|
||||||
name: 'com.affine.page-properties.property.docPrimaryMode',
|
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<
|
} as Record<
|
||||||
string,
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -14,9 +14,9 @@ import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-
|
|||||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||||
import { useRegisterCopyLinkCommands } from '@affine/core/components/hooks/affine/use-register-copy-link-commands';
|
import { useRegisterCopyLinkCommands } from '@affine/core/components/hooks/affine/use-register-copy-link-commands';
|
||||||
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
|
import { useDocCollectionPageTitle } from '@affine/core/components/hooks/use-block-suite-workspace-page-title';
|
||||||
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
|
|
||||||
import { HeaderDivider } from '@affine/core/components/pure/header';
|
import { HeaderDivider } from '@affine/core/components/pure/header';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
|
||||||
import type { Doc } from '@blocksuite/affine/store';
|
import type { Doc } from '@blocksuite/affine/store';
|
||||||
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
import { useLiveData, useService, type Workspace } from '@toeverything/infra';
|
||||||
@@ -155,7 +155,8 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
|||||||
|
|
||||||
export function DetailPageHeader(props: PageHeaderProps) {
|
export function DetailPageHeader(props: PageHeaderProps) {
|
||||||
const { page, workspace } = props;
|
const { page, workspace } = props;
|
||||||
const { isJournal } = useJournalInfoHelper(page.id);
|
const journalService = useService(JournalService);
|
||||||
|
const isJournal = !!useLiveData(journalService.journalDate$(page.id));
|
||||||
const isInTrash = page.meta?.trash;
|
const isInTrash = page.meta?.trash;
|
||||||
|
|
||||||
useRegisterCopyLinkCommands({
|
useRegisterCopyLinkCommands({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
const interactive = style({
|
const interactive = style({
|
||||||
@@ -161,6 +162,9 @@ export const pageItemLabel = style({
|
|||||||
color: cssVar('primaryColor'),
|
color: cssVar('primaryColor'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
display: 'flex',
|
||||||
|
gap: 6,
|
||||||
|
alignItems: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
// conflict
|
// conflict
|
||||||
@@ -185,6 +189,16 @@ export const journalConflictMoreTrigger = style([
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
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,
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
lineHeight: '20px',
|
||||||
|
fontWeight: 400,
|
||||||
|
});
|
||||||
|
|
||||||
// customize date-picker cell
|
// customize date-picker cell
|
||||||
export const journalDateCell = style([
|
export const journalDateCell = style([
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import type { DateCell } from '@affine/component';
|
import type { DateCell } from '@affine/component';
|
||||||
import { DatePicker, IconButton, Menu, Scrollable } from '@affine/component';
|
|
||||||
import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper';
|
|
||||||
import {
|
import {
|
||||||
useJournalHelper,
|
DatePicker,
|
||||||
useJournalInfoHelper,
|
IconButton,
|
||||||
useJournalRouteHelper,
|
Menu,
|
||||||
} from '@affine/core/components/hooks/use-journal';
|
MenuItem,
|
||||||
|
MenuSeparator,
|
||||||
|
Scrollable,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { useTrashModalHelper } from '@affine/core/components/hooks/affine/use-trash-modal-helper';
|
||||||
|
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||||
import { MoveToTrash } from '@affine/core/components/page-list';
|
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
import { CalendarXmarkIcon, EditIcon } from '@blocksuite/icons/rc';
|
||||||
import type { DocRecord } from '@toeverything/infra';
|
import type { DocRecord } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
DocService,
|
DocService,
|
||||||
@@ -41,8 +45,15 @@ interface PageItemProps
|
|||||||
extends Omit<HTMLAttributes<HTMLAnchorElement>, 'onClick'> {
|
extends Omit<HTMLAttributes<HTMLAnchorElement>, 'onClick'> {
|
||||||
docId: string;
|
docId: string;
|
||||||
right?: ReactNode;
|
right?: ReactNode;
|
||||||
|
duplicate?: boolean;
|
||||||
}
|
}
|
||||||
const PageItem = ({ docId, right, className, ...attrs }: PageItemProps) => {
|
const PageItem = ({
|
||||||
|
docId,
|
||||||
|
right,
|
||||||
|
duplicate,
|
||||||
|
className,
|
||||||
|
...attrs
|
||||||
|
}: PageItemProps) => {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const Icon = useLiveData(
|
const Icon = useLiveData(
|
||||||
@@ -53,6 +64,7 @@ const PageItem = ({ docId, right, className, ...attrs }: PageItemProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkbenchLink
|
<WorkbenchLink
|
||||||
|
data-testid="journal-conflict-item"
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
to={`/${docId}`}
|
to={`/${docId}`}
|
||||||
className={clsx(className, styles.pageItem)}
|
className={clsx(className, styles.pageItem)}
|
||||||
@@ -61,7 +73,14 @@ const PageItem = ({ docId, right, className, ...attrs }: PageItemProps) => {
|
|||||||
<div className={styles.pageItemIcon}>
|
<div className={styles.pageItemIcon}>
|
||||||
<Icon width={20} height={20} />
|
<Icon width={20} height={20} />
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.pageItemLabel}>{title}</span>
|
<div className={styles.pageItemLabel}>
|
||||||
|
{title}
|
||||||
|
{duplicate ? (
|
||||||
|
<div className={styles.duplicateTag}>
|
||||||
|
{i18n['com.affine.page-properties.property.journal-duplicated']()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
{right}
|
{right}
|
||||||
</WorkbenchLink>
|
</WorkbenchLink>
|
||||||
);
|
);
|
||||||
@@ -82,7 +101,10 @@ export const EditorJournalPanel = () => {
|
|||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const doc = useService(DocService).doc;
|
const doc = useService(DocService).doc;
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const { journalDate, isJournal } = useJournalInfoHelper(doc.id);
|
const journalService = useService(JournalService);
|
||||||
|
const journalDateStr = useLiveData(journalService.journalDate$(doc.id));
|
||||||
|
const journalDate = journalDateStr ? dayjs(journalDateStr) : null;
|
||||||
|
const isJournal = !!journalDate;
|
||||||
const { openJournal } = useJournalRouteHelper(workspace.docCollection);
|
const { openJournal } = useJournalRouteHelper(workspace.docCollection);
|
||||||
|
|
||||||
const onDateSelect = useCallback(
|
const onDateSelect = useCallback(
|
||||||
@@ -93,12 +115,11 @@ export const EditorJournalPanel = () => {
|
|||||||
[journalDate, openJournal]
|
[journalDate, openJournal]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const allJournalDates = useLiveData(journalService.allJournalDates$);
|
||||||
|
|
||||||
const customDayRenderer = useCallback(
|
const customDayRenderer = useCallback(
|
||||||
(cell: DateCell) => {
|
(cell: DateCell) => {
|
||||||
// TODO(@catsjuice): add a dot to indicate journal
|
const hasJournal = allJournalDates.has(cell.date.format('YYYY-MM-DD'));
|
||||||
// has performance issue for now, better to calculate it in advance
|
|
||||||
// const hasJournal = !!getJournalsByDate(cell.date.format('YYYY-MM-DD'))?.length;
|
|
||||||
const hasJournal = false;
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={styles.journalDateCell}
|
className={styles.journalDateCell}
|
||||||
@@ -118,11 +139,15 @@ export const EditorJournalPanel = () => {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[isJournal]
|
[allJournalDates, isJournal]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.journalPanel} data-is-journal={isJournal}>
|
<div
|
||||||
|
className={styles.journalPanel}
|
||||||
|
data-is-journal={isJournal}
|
||||||
|
data-testid="sidebar-journal-panel"
|
||||||
|
>
|
||||||
<div data-mobile={mobile} className={styles.calendar}>
|
<div data-mobile={mobile} className={styles.calendar}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
weekDays={t['com.affine.calendar-date-picker.week-days']()}
|
weekDays={t['com.affine.calendar-date-picker.week-days']()}
|
||||||
@@ -284,7 +309,9 @@ const ConflictList = ({
|
|||||||
className,
|
className,
|
||||||
...attrs
|
...attrs
|
||||||
}: ConflictListProps) => {
|
}: ConflictListProps) => {
|
||||||
|
const t = useI18n();
|
||||||
const currentDoc = useService(DocService).doc;
|
const currentDoc = useService(DocService).doc;
|
||||||
|
const journalService = useService(JournalService);
|
||||||
const { setTrashModal } = useTrashModalHelper();
|
const { setTrashModal } = useTrashModalHelper();
|
||||||
|
|
||||||
const handleOpenTrashModal = useCallback(
|
const handleOpenTrashModal = useCallback(
|
||||||
@@ -297,9 +324,19 @@ const ConflictList = ({
|
|||||||
},
|
},
|
||||||
[setTrashModal]
|
[setTrashModal]
|
||||||
);
|
);
|
||||||
|
const handleRemoveJournalMark = useCallback(
|
||||||
|
(docId: string) => {
|
||||||
|
journalService.removeJournalDate(docId);
|
||||||
|
},
|
||||||
|
[journalService]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.journalConflictWrapper, className)} {...attrs}>
|
<div
|
||||||
|
data-testid="journal-conflict-list"
|
||||||
|
className={clsx(styles.journalConflictWrapper, className)}
|
||||||
|
{...attrs}
|
||||||
|
>
|
||||||
{docRecords.map(docRecord => {
|
{docRecords.map(docRecord => {
|
||||||
const isCurrent = docRecord.id === currentDoc.id;
|
const isCurrent = docRecord.id === currentDoc.id;
|
||||||
return (
|
return (
|
||||||
@@ -307,17 +344,40 @@ const ConflictList = ({
|
|||||||
aria-selected={isCurrent}
|
aria-selected={isCurrent}
|
||||||
docId={docRecord.id}
|
docId={docRecord.id}
|
||||||
key={docRecord.id}
|
key={docRecord.id}
|
||||||
|
duplicate
|
||||||
right={
|
right={
|
||||||
<Menu
|
<Menu
|
||||||
|
contentOptions={{
|
||||||
|
style: { width: 237, maxWidth: '100%' },
|
||||||
|
align: 'end',
|
||||||
|
alignOffset: -4,
|
||||||
|
sideOffset: 8,
|
||||||
|
}}
|
||||||
items={
|
items={
|
||||||
<MoveToTrash
|
<>
|
||||||
onSelect={() => handleOpenTrashModal(docRecord)}
|
<MenuItem
|
||||||
/>
|
prefixIcon={<CalendarXmarkIcon />}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemoveJournalMark(docRecord.id);
|
||||||
|
}}
|
||||||
|
data-testid="journal-conflict-remove-mark"
|
||||||
|
>
|
||||||
|
{t[
|
||||||
|
'com.affine.page-properties.property.journal-remove'
|
||||||
|
]()}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuSeparator />
|
||||||
|
<MoveToTrash
|
||||||
|
onSelect={() => handleOpenTrashModal(docRecord)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton>
|
<IconButton
|
||||||
<MoreHorizontalIcon />
|
data-testid="journal-conflict-edit"
|
||||||
</IconButton>
|
icon={<EditIcon />}
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -329,10 +389,15 @@ const ConflictList = ({
|
|||||||
};
|
};
|
||||||
const JournalConflictBlock = ({ date }: JournalBlockProps) => {
|
const JournalConflictBlock = ({ date }: JournalBlockProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
|
||||||
const docRecordList = useService(DocsService).list;
|
const docRecordList = useService(DocsService).list;
|
||||||
const journalHelper = useJournalHelper(workspace.docCollection);
|
const journalService = useService(JournalService);
|
||||||
const docs = journalHelper.getJournalsByDate(date.format('YYYY-MM-DD'));
|
const dateString = date.format('YYYY-MM-DD');
|
||||||
|
const docs = useLiveData(
|
||||||
|
useMemo(
|
||||||
|
() => journalService.journalsByDate$(dateString),
|
||||||
|
[dateString, journalService]
|
||||||
|
)
|
||||||
|
);
|
||||||
const docRecords = useLiveData(
|
const docRecords = useLiveData(
|
||||||
docRecordList.docs$.map(records =>
|
docRecordList.docs$.map(records =>
|
||||||
records.filter(v => {
|
records.filter(v => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LiveData, Service } from '@toeverything/infra';
|
import { LiveData, Service } from '@toeverything/infra';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import type { JournalStore } from '../store/journal';
|
import type { JournalStore } from '../store/journal';
|
||||||
|
|
||||||
@@ -7,15 +8,31 @@ export class JournalService extends Service {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allJournalDates$ = this.store.allJournalDates$;
|
||||||
|
|
||||||
journalDate$(docId: string) {
|
journalDate$(docId: string) {
|
||||||
return LiveData.from(this.store.watchDocJournalDate(docId), undefined);
|
return LiveData.from(this.store.watchDocJournalDate(docId), undefined);
|
||||||
}
|
}
|
||||||
|
journalToday$(docId: string) {
|
||||||
|
return LiveData.computed(get => {
|
||||||
|
const date = get(this.journalDate$(docId));
|
||||||
|
if (!date) return false;
|
||||||
|
return dayjs(date).isSame(dayjs(), 'day');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setJournalDate(docId: string, date: string) {
|
setJournalDate(docId: string, date: string) {
|
||||||
this.store.setDocJournalDate(docId, date);
|
this.store.setDocJournalDate(docId, date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeJournalDate(docId: string) {
|
||||||
|
this.store.removeDocJournalDate(docId);
|
||||||
|
}
|
||||||
|
|
||||||
getJournalsByDate(date: string) {
|
getJournalsByDate(date: string) {
|
||||||
return this.store.getDocsByJournalDate(date);
|
return this.store.getDocsByJournalDate(date);
|
||||||
}
|
}
|
||||||
|
journalsByDate$(date: string) {
|
||||||
|
return this.store.docsByJournalDate$(date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ export class JournalStore extends Store {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allJournalDates$ = LiveData.computed(get => {
|
||||||
|
return new Set(
|
||||||
|
get(this.docsService.list.docs$)
|
||||||
|
.filter(doc => {
|
||||||
|
const journal = get(doc.properties$.selector(p => p.journal));
|
||||||
|
return !!journal && isJournalString(journal);
|
||||||
|
})
|
||||||
|
.map(doc => get(doc.properties$.selector(p => p.journal)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
watchDocJournalDate(docId: string): Observable<string | undefined> {
|
watchDocJournalDate(docId: string): Observable<string | undefined> {
|
||||||
return LiveData.computed(get => {
|
return LiveData.computed(get => {
|
||||||
const doc = get(this.docsService.list.doc$(docId));
|
const doc = get(this.docsService.list.doc$(docId));
|
||||||
@@ -35,9 +46,21 @@ export class JournalStore extends Store {
|
|||||||
doc.setProperty('journal', date);
|
doc.setProperty('journal', date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeDocJournalDate(docId: string) {
|
||||||
|
this.setDocJournalDate(docId, '');
|
||||||
|
}
|
||||||
|
|
||||||
getDocsByJournalDate(date: string) {
|
getDocsByJournalDate(date: string) {
|
||||||
return this.docsService.list.docs$.value.filter(
|
return this.docsService.list.docs$.value.filter(
|
||||||
doc => doc.properties$.value.journal === date
|
doc => doc.properties$.value.journal === date
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
docsByJournalDate$(date: string) {
|
||||||
|
return LiveData.computed(get => {
|
||||||
|
return get(this.docsService.list.docs$).filter(doc => {
|
||||||
|
const journal = get(doc.properties$.selector(p => p.journal));
|
||||||
|
return journal === date;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
"ru": 84,
|
"ru": 84,
|
||||||
"sv-SE": 5,
|
"sv-SE": 5,
|
||||||
"ur": 3,
|
"ur": 3,
|
||||||
"zh-Hans": 100,
|
"zh-Hans": 99,
|
||||||
"zh-Hant": 99
|
"zh-Hant": 99
|
||||||
}
|
}
|
||||||
@@ -652,6 +652,9 @@
|
|||||||
"com.affine.page-properties.property.tags": "Tags",
|
"com.affine.page-properties.property.tags": "Tags",
|
||||||
"com.affine.page-properties.property.docPrimaryMode": "Doc mode",
|
"com.affine.page-properties.property.docPrimaryMode": "Doc mode",
|
||||||
"com.affine.page-properties.property.text": "Text",
|
"com.affine.page-properties.property.text": "Text",
|
||||||
|
"com.affine.page-properties.property.journal": "Journal",
|
||||||
|
"com.affine.page-properties.property.journal-duplicated": "Duplicated",
|
||||||
|
"com.affine.page-properties.property.journal-remove": "Remove journal mark",
|
||||||
"com.affine.page-properties.property.updatedBy": "Last edited by",
|
"com.affine.page-properties.property.updatedBy": "Last edited by",
|
||||||
"com.affine.propertySidebar.property-list.section": "Properties",
|
"com.affine.propertySidebar.property-list.section": "Properties",
|
||||||
"com.affine.propertySidebar.add-more.section": "Add more properties",
|
"com.affine.propertySidebar.add-more.section": "Add more properties",
|
||||||
|
|||||||
122
tests/affine-local/e2e/journal.spec.ts
Normal file
122
tests/affine-local/e2e/journal.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { test } from '@affine-test/kit/playwright';
|
||||||
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
|
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
|
||||||
|
import { expect, type Locator, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
type MaybeDate = string | number | Date;
|
||||||
|
function isSameDay(d1: MaybeDate, d2: MaybeDate) {
|
||||||
|
const date1 = new Date(d1);
|
||||||
|
const date2 = new Date(d2);
|
||||||
|
return (
|
||||||
|
date1.getFullYear() === date2.getFullYear() &&
|
||||||
|
date1.getMonth() === date2.getMonth() &&
|
||||||
|
date1.getDate() === date2.getDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJournalRow(page: Page) {
|
||||||
|
return page.locator(
|
||||||
|
'[data-testid="doc-property-row"][data-info-id="journal"]'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function isJournalEditor(page: Page, maybeDate?: string | number | Date) {
|
||||||
|
// journal header
|
||||||
|
const header = page.getByTestId('header');
|
||||||
|
const weekPicker = header.getByTestId('journal-week-picker');
|
||||||
|
await expect(weekPicker).toBeVisible();
|
||||||
|
|
||||||
|
// journal title
|
||||||
|
const journalTitle = page.getByTestId('journal-title');
|
||||||
|
await expect(journalTitle).toBeVisible();
|
||||||
|
|
||||||
|
if (maybeDate) {
|
||||||
|
const date = (await journalTitle.getByTestId('date').textContent()) ?? '';
|
||||||
|
expect(isSameDay(date, maybeDate)).toBeTruthy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function openPagePropertiesAndAddJournal(page: Page) {
|
||||||
|
const collapse = page.getByTestId('page-info-collapse');
|
||||||
|
const open = await collapse.getAttribute('aria-expanded');
|
||||||
|
if (open?.toLowerCase() !== 'true') {
|
||||||
|
await collapse.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// add if not exists
|
||||||
|
if ((await getJournalRow(page).count()) === 0) {
|
||||||
|
const addPropertyButton = page.getByTestId('add-property-button');
|
||||||
|
if (!(await addPropertyButton.isVisible())) {
|
||||||
|
await page.getByTestId('property-collapsible-button').click();
|
||||||
|
}
|
||||||
|
await addPropertyButton.click();
|
||||||
|
await page
|
||||||
|
.locator('[role="menuitem"][data-property-type="journal"]')
|
||||||
|
.click();
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
}
|
||||||
|
// expand if collapsed
|
||||||
|
else if (!(await getJournalRow(page).isVisible())) {
|
||||||
|
await page.getByTestId('property-collapsible-button').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
const journalRow = getJournalRow(page);
|
||||||
|
await expect(journalRow).toBeVisible();
|
||||||
|
return journalRow;
|
||||||
|
}
|
||||||
|
async function toggleJournal(row: Locator, value: boolean) {
|
||||||
|
const checkbox = row.locator('input[type="checkbox"]');
|
||||||
|
const state = await checkbox.inputValue();
|
||||||
|
const checked = state === 'on';
|
||||||
|
if (checked !== value) {
|
||||||
|
await checkbox.click();
|
||||||
|
const newState = await checkbox.inputValue();
|
||||||
|
const newChecked = newState === 'on';
|
||||||
|
expect(newChecked).toBe(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function createPageAndTurnIntoJournal(page: Page) {
|
||||||
|
await page.getByTestId('sidebar-new-page-button').click();
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
const journalRow = await openPagePropertiesAndAddJournal(page);
|
||||||
|
await toggleJournal(journalRow, true);
|
||||||
|
return journalRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Create a journal from sidebar', async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await page.getByTestId('slider-bar-journals-button').click();
|
||||||
|
await waitForEditorLoad(page);
|
||||||
|
await isJournalEditor(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create a page and turn it into a journal', async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await createPageAndTurnIntoJournal(page);
|
||||||
|
await isJournalEditor(page, new Date());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should show duplicated tag when create journal on same day', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await createPageAndTurnIntoJournal(page);
|
||||||
|
const journalRow2 = await createPageAndTurnIntoJournal(page);
|
||||||
|
await expect(journalRow2.getByTestId('conflict-tag')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Resolve duplicated journal', async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await createPageAndTurnIntoJournal(page);
|
||||||
|
const journalRow2 = await createPageAndTurnIntoJournal(page);
|
||||||
|
await journalRow2.getByTestId('conflict-tag').click();
|
||||||
|
const journalPanel = page.getByTestId('sidebar-journal-panel');
|
||||||
|
await expect(journalPanel).toBeVisible();
|
||||||
|
const conflictList = journalPanel.getByTestId('journal-conflict-list');
|
||||||
|
await expect(conflictList).toBeVisible();
|
||||||
|
const conflictItems = conflictList.getByTestId('journal-conflict-item');
|
||||||
|
const first = conflictItems.first();
|
||||||
|
await first.getByTestId('journal-conflict-edit').click();
|
||||||
|
await page.getByTestId('journal-conflict-remove-mark').click();
|
||||||
|
|
||||||
|
await expect(journalRow2.getByTestId('conflict-tag')).not.toBeVisible();
|
||||||
|
await expect(conflictList).not.toBeVisible();
|
||||||
|
});
|
||||||
@@ -120,6 +120,7 @@ test('property table reordering', async ({ page }) => {
|
|||||||
|
|
||||||
// new order should be Doc mode, (Tags), Number, Date, Checkbox, Text
|
// new order should be Doc mode, (Tags), Number, Date, Checkbox, Text
|
||||||
for (const [index, property] of [
|
for (const [index, property] of [
|
||||||
|
'Journal',
|
||||||
'Doc mode',
|
'Doc mode',
|
||||||
'Tags',
|
'Tags',
|
||||||
'Number',
|
'Number',
|
||||||
@@ -159,6 +160,7 @@ test('page info show more will show all properties', async ({ page }) => {
|
|||||||
await page.click('[data-testid="property-collapsible-button"]');
|
await page.click('[data-testid="property-collapsible-button"]');
|
||||||
|
|
||||||
for (const [index, property] of [
|
for (const [index, property] of [
|
||||||
|
'Journal',
|
||||||
'Doc mode',
|
'Doc mode',
|
||||||
'Tags',
|
'Tags',
|
||||||
'Text',
|
'Text',
|
||||||
|
|||||||
Reference in New Issue
Block a user