diff --git a/packages/frontend/component/src/ui/menu/mobile/root.tsx b/packages/frontend/component/src/ui/menu/mobile/root.tsx index 4586d5c58424a..f7772c8a5e4fa 100644 --- a/packages/frontend/component/src/ui/menu/mobile/root.tsx +++ b/packages/frontend/component/src/ui/menu/mobile/root.tsx @@ -7,6 +7,7 @@ import { useCallback, useContext, useEffect, useState } from 'react'; import { observeResize } from '../../../utils'; import { Button } from '../../button'; import { Modal } from '../../modal'; +import { Scrollable } from '../../scrollbar'; import type { MenuProps } from '../menu.types'; import type { SubMenuContent } from './context'; import { MobileMenuContext } from './context'; @@ -138,8 +139,12 @@ export const MobileMenu = ({ > {sub.title || t['com.affine.backButton']()} - - {sub.items} + + + {sub.items} + + + ))} diff --git a/packages/frontend/component/src/ui/menu/mobile/styles.css.ts b/packages/frontend/component/src/ui/menu/mobile/styles.css.ts index 64a57bc54ffdb..93970358d9432 100644 --- a/packages/frontend/component/src/ui/menu/mobile/styles.css.ts +++ b/packages/frontend/component/src/ui/menu/mobile/styles.css.ts @@ -3,7 +3,7 @@ import { cssVarV2 } from '@toeverything/theme/v2'; import { style } from '@vanilla-extract/css'; import { modalContent } from '../../modal/styles.css'; -import { bgColor } from '../styles.css'; +import { bgColor, iconColor, labelColor } from '../styles.css'; // To override desktop menu style defined in '../styles.css.ts' @@ -55,6 +55,12 @@ export const mobileMenuItem = style({ }, }, selectors: { + '&.danger': { + vars: { + [labelColor]: cssVarV2('button/error'), + [iconColor]: cssVarV2('button/error'), + }, + }, '&.danger:hover': { vars: { [bgColor]: 'transparent' }, }, @@ -97,3 +103,7 @@ export const backButton = style({ paddingLeft: 0, marginLeft: 20, }); + +export const scrollArea = style({ + maxHeight: '80dvh', +}); diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.tsx index d9b8917926414..63be78c04bbe1 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/tabs/journal.tsx @@ -168,7 +168,7 @@ export const EditorJournalPanel = () => { ); }; -const sortPagesByDate = ( +export const sortPagesByDate = ( docs: DocRecord[], field: 'updatedDate' | 'createDate', order: 'asc' | 'desc' = 'desc' diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-conflicts.css.ts b/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-conflicts.css.ts new file mode 100644 index 0000000000000..d74c537433f74 --- /dev/null +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-conflicts.css.ts @@ -0,0 +1,19 @@ +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const docItem = style({ + display: 'flex', + alignItems: 'center', + gap: 8, +}); + +export const duplicateTag = style({ + borderRadius: 4, + padding: '0 8px', + fontSize: cssVar('fontXs'), + lineHeight: '20px', + color: cssVarV2('toast/iconState/error'), + background: cssVarV2('layer/background/error'), + border: `1px solid ${cssVarV2('database/border')}`, +}); diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-conflicts.tsx b/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-conflicts.tsx new file mode 100644 index 0000000000000..cced18999823c --- /dev/null +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-conflicts.tsx @@ -0,0 +1,154 @@ +import { + IconButton, + MobileMenu, + MobileMenuItem, + MobileMenuSub, + useConfirmModal, +} from '@affine/component'; +import { MoveToTrash } from '@affine/core/components/page-list'; +import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; +import { JournalService } from '@affine/core/modules/journal'; +import { WorkbenchLink } from '@affine/core/modules/workbench'; +import { useI18n } from '@affine/i18n'; +import { CalendarXmarkIcon, EditIcon, TodayIcon } from '@blocksuite/icons/rc'; +import type { DocRecord } from '@toeverything/infra'; +import { + DocService, + DocsService, + useLiveData, + useService, +} from '@toeverything/infra'; +import { type MouseEvent, useCallback, useMemo } from 'react'; + +import * as styles from './journal-conflicts.css'; + +const ResolveConflictOperations = ({ docRecord }: { docRecord: DocRecord }) => { + const t = useI18n(); + const journalService = useService(JournalService); + const { openConfirmModal } = useConfirmModal(); + + const handleOpenTrashModal = useCallback( + (docRecord: DocRecord) => { + openConfirmModal({ + title: t['com.affine.moveToTrash.confirmModal.title'](), + description: t['com.affine.moveToTrash.confirmModal.description']({ + title: docRecord.title$.value || t['Untitled'](), + }), + cancelText: t['com.affine.confirmModal.button.cancel'](), + confirmText: t.Delete(), + onConfirm: () => { + docRecord.moveToTrash(); + }, + }); + }, + [openConfirmModal, t] + ); + const handleRemoveJournalMark = useCallback( + (docId: string) => { + journalService.removeJournalDate(docId); + }, + [journalService] + ); + + return ( + <> + } + onClick={() => { + handleRemoveJournalMark(docRecord.id); + }} + data-testid="journal-conflict-remove-mark" + > + {t['com.affine.page-properties.property.journal-remove']()} + + handleOpenTrashModal(docRecord)} /> + + ); +}; + +const preventNav = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); +}; + +const DocItem = ({ docRecord }: { docRecord: DocRecord }) => { + const docId = docRecord.id; + const i18n = useI18n(); + const docDisplayMetaService = useService(DocDisplayMetaService); + const Icon = useLiveData( + docDisplayMetaService.icon$(docId, { compareDate: new Date() }) + ); + const titleMeta = useLiveData(docDisplayMetaService.title$(docId)); + const title = i18n.t(titleMeta); + return ( + + } + suffix={ + } + > + } /> + + } + > +
+ {title} +
+ {i18n['com.affine.page-properties.property.journal-duplicated']()} +
+
+
+
+ ); +}; + +const ConflictList = ({ docRecords }: { docRecords: DocRecord[] }) => { + return docRecords.map(docRecord => ( + + )); +}; + +const ConflictListMenuItem = ({ docRecords }: { docRecords: DocRecord[] }) => { + const t = useI18n(); + return ( + , + type: 'danger', + }} + items={} + > + {t['com.affine.m.selector.journal-menu.conflicts']()} + + ); +}; + +const JournalConflictsChecker = ({ date }: { date: string }) => { + const docRecordList = useService(DocsService).list; + const journalService = useService(JournalService); + const docs = useLiveData( + useMemo(() => journalService.journalsByDate$(date), [journalService, date]) + ); + const docRecords = useLiveData( + docRecordList.docs$.map(records => + records.filter(v => { + return docs.some(doc => doc.id === v.id); + }) + ) + ); + + if (docRecords.length <= 1) return null; + + return ; +}; + +export const JournalConflictsMenuItem = () => { + const journalService = useService(JournalService); + const docId = useService(DocService).doc.id; + const journalDate = useLiveData(journalService.journalDate$(docId)); + + if (!journalDate) return null; + + return ; +}; diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-today-activity.css.ts b/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-today-activity.css.ts new file mode 100644 index 0000000000000..0b67403b95942 --- /dev/null +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-today-activity.css.ts @@ -0,0 +1,18 @@ +import { bodyEmphasized, bodyRegular } from '@toeverything/theme/typography'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { style } from '@vanilla-extract/css'; + +export const title = style([ + bodyEmphasized, + { + padding: '11px 20px', + }, +]); + +export const empty = style([ + bodyRegular, + { + padding: '11px 20px', + color: cssVarV2('text/placeholder'), + }, +]); diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-today-activity.tsx b/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-today-activity.tsx new file mode 100644 index 0000000000000..a4942a144b6e4 --- /dev/null +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-today-activity.tsx @@ -0,0 +1,135 @@ +import { MenuItem, MenuSeparator, MobileMenuSub } from '@affine/component'; +import { sortPagesByDate } from '@affine/core/desktop/pages/workspace/detail-page/tabs/journal'; +import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; +import { JournalService } from '@affine/core/modules/journal'; +import { WorkbenchLink } from '@affine/core/modules/workbench'; +import { useI18n } from '@affine/i18n'; +import { HistoryIcon } from '@blocksuite/icons/rc'; +import type { DocRecord } from '@toeverything/infra'; +import { + DocService, + DocsService, + useLiveData, + useService, +} from '@toeverything/infra'; +import dayjs from 'dayjs'; +import { type ReactNode, useCallback, useMemo } from 'react'; + +import * as styles from './journal-today-activity.css'; + +interface JournalTodayActivityMenuItemProps { + prefix?: ReactNode; + suffix?: ReactNode; +} +type Category = 'created' | 'updated'; + +const DocItem = ({ docId }: { docId: string }) => { + const i18n = useI18n(); + const docDisplayMetaService = useService(DocDisplayMetaService); + const Icon = useLiveData( + docDisplayMetaService.icon$(docId, { compareDate: new Date() }) + ); + const titleMeta = useLiveData(docDisplayMetaService.title$(docId)); + const title = i18n.t(titleMeta); + return ( + + }>{title} + + ); +}; + +const ActivityBlock = ({ + name, + list, +}: { + name: Category; + list: DocRecord[]; +}) => { + const t = useI18n(); + + const title = + name === 'created' + ? t['com.affine.journal.created-today']() + : t['com.affine.journal.updated-today'](); + return ( + <> +
{title}
+ {list.length > 0 ? ( + list.map(doc => { + return ; + }) + ) : ( +
+ {name === 'created' + ? t['com.affine.journal.daily-count-created-empty-tips']() + : t['com.affine.journal.daily-count-updated-empty-tips']()} +
+ )} + + ); +}; + +const TodaysActivity = ({ date }: { date: string }) => { + const docRecords = useLiveData(useService(DocsService).list.docs$); + const getTodaysPages = useCallback( + (field: 'createDate' | 'updatedDate') => { + return sortPagesByDate( + docRecords.filter(docRecord => { + const meta = docRecord.meta$.value; + if (meta.trash) return false; + return meta[field] && dayjs(meta[field]).isSame(date, 'day'); + }), + field + ); + }, + [date, docRecords] + ); + + const createdToday = useMemo( + () => getTodaysPages('createDate'), + [getTodaysPages] + ); + const updatedToday = useMemo( + () => getTodaysPages('updatedDate'), + [getTodaysPages] + ); + + return ( + <> + + + + + ); +}; + +export const JournalTodayActivityMenuItem = ({ + prefix, + suffix, +}: JournalTodayActivityMenuItemProps) => { + const docService = useService(DocService); + const journalService = useService(JournalService); + + const docId = docService.doc.id; + const journalDate = useLiveData(journalService.journalDate$(docId)); + + const t = useI18n(); + + if (!journalDate) return null; + + return ( + <> + {prefix} + , + }} + items={} + title={t['com.affine.m.selector.journal-menu.today-activity']()} + > + {t['com.affine.m.selector.journal-menu.today-activity']()} + + {suffix} + + ); +}; diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-more-button.tsx b/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-more-button.tsx index 321cf554253d5..713c8901d03a7 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-more-button.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/page-header-more-button.tsx @@ -22,6 +22,8 @@ import { import { DocService, useLiveData, useService } from '@toeverything/infra'; import { useCallback, useEffect, useState } from 'react'; +import { JournalConflictsMenuItem } from './menu/journal-conflicts'; +import { JournalTodayActivityMenuItem } from './menu/journal-today-activity'; import * as styles from './page-header-more-button.css'; import { DocInfoSheet } from './sheets/doc-info'; @@ -82,6 +84,7 @@ export const PageHeaderMenuButton = () => { const EditMenu = ( <> + } /> : } data-testid="editor-option-menu-mode-switch" @@ -120,6 +123,7 @@ export const PageHeaderMenuButton = () => { {t['com.affine.header.option.view-toc']()} + ); if (isInTrash) { diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 7841a58857092..45df90c856fd8 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,5 +1,5 @@ { - "ar": 80, + "ar": 79, "ca": 6, "da": 6, "de": 30, diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index e688fa4555de4..082961a318c98 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1391,5 +1391,7 @@ "com.affine.m.selector.remove-warning.confirm": "Do not ask again", "com.affine.m.selector.remove-warning.cancel": "Cancel", "com.affine.m.selector.remove-warning.where-tag": "tag", - "com.affine.m.selector.remove-warning.where-folder": "folder" + "com.affine.m.selector.remove-warning.where-folder": "folder", + "com.affine.m.selector.journal-menu.today-activity": "Today's activity", + "com.affine.m.selector.journal-menu.conflicts": "Duplicate Entries in Today's Journal" }