-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(mobile): jouranl daily activity and conflict operations
- Loading branch information
Showing
10 changed files
with
353 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-conflicts.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')}`, | ||
}); |
154 changes: 154 additions & 0 deletions
154
packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-conflicts.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<> | ||
<MobileMenuItem | ||
prefixIcon={<CalendarXmarkIcon />} | ||
onClick={() => { | ||
handleRemoveJournalMark(docRecord.id); | ||
}} | ||
data-testid="journal-conflict-remove-mark" | ||
> | ||
{t['com.affine.page-properties.property.journal-remove']()} | ||
</MobileMenuItem> | ||
<MoveToTrash onSelect={() => handleOpenTrashModal(docRecord)} /> | ||
</> | ||
); | ||
}; | ||
|
||
const preventNav = (e: MouseEvent<HTMLButtonElement>) => { | ||
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 ( | ||
<WorkbenchLink aria-label={title} to={`/${docId}`}> | ||
<MobileMenuItem | ||
prefixIcon={<Icon />} | ||
suffix={ | ||
<MobileMenu | ||
items={<ResolveConflictOperations docRecord={docRecord} />} | ||
> | ||
<IconButton onClick={preventNav} icon={<EditIcon />} /> | ||
</MobileMenu> | ||
} | ||
> | ||
<div className={styles.docItem}> | ||
{title} | ||
<div className={styles.duplicateTag}> | ||
{i18n['com.affine.page-properties.property.journal-duplicated']()} | ||
</div> | ||
</div> | ||
</MobileMenuItem> | ||
</WorkbenchLink> | ||
); | ||
}; | ||
|
||
const ConflictList = ({ docRecords }: { docRecords: DocRecord[] }) => { | ||
return docRecords.map(docRecord => ( | ||
<DocItem key={docRecord.id} docRecord={docRecord} /> | ||
)); | ||
}; | ||
|
||
const ConflictListMenuItem = ({ docRecords }: { docRecords: DocRecord[] }) => { | ||
const t = useI18n(); | ||
return ( | ||
<MobileMenuSub | ||
triggerOptions={{ | ||
prefixIcon: <TodayIcon />, | ||
type: 'danger', | ||
}} | ||
items={<ConflictList docRecords={docRecords} />} | ||
> | ||
{t['com.affine.m.selector.journal-menu.conflicts']()} | ||
</MobileMenuSub> | ||
); | ||
}; | ||
|
||
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 <ConflictListMenuItem docRecords={docRecords} />; | ||
}; | ||
|
||
export const JournalConflictsMenuItem = () => { | ||
const journalService = useService(JournalService); | ||
const docId = useService(DocService).doc.id; | ||
const journalDate = useLiveData(journalService.journalDate$(docId)); | ||
|
||
if (!journalDate) return null; | ||
|
||
return <JournalConflictsChecker date={journalDate} />; | ||
}; |
18 changes: 18 additions & 0 deletions
18
packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-today-activity.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'), | ||
}, | ||
]); |
135 changes: 135 additions & 0 deletions
135
packages/frontend/core/src/mobile/pages/workspace/detail/menu/journal-today-activity.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<WorkbenchLink aria-label={title} to={`/${docId}`}> | ||
<MenuItem prefixIcon={<Icon />}>{title}</MenuItem> | ||
</WorkbenchLink> | ||
); | ||
}; | ||
|
||
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 ( | ||
<> | ||
<div className={styles.title}>{title}</div> | ||
{list.length > 0 ? ( | ||
list.map(doc => { | ||
return <DocItem docId={doc.id} key={doc.id} />; | ||
}) | ||
) : ( | ||
<div className={styles.empty}> | ||
{name === 'created' | ||
? t['com.affine.journal.daily-count-created-empty-tips']() | ||
: t['com.affine.journal.daily-count-updated-empty-tips']()} | ||
</div> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
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 ( | ||
<> | ||
<ActivityBlock name="created" list={createdToday} /> | ||
<MenuSeparator /> | ||
<ActivityBlock name="updated" list={updatedToday} /> | ||
</> | ||
); | ||
}; | ||
|
||
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} | ||
<MobileMenuSub | ||
triggerOptions={{ | ||
prefixIcon: <HistoryIcon />, | ||
}} | ||
items={<TodaysActivity date={journalDate} />} | ||
title={t['com.affine.m.selector.journal-menu.today-activity']()} | ||
> | ||
{t['com.affine.m.selector.journal-menu.today-activity']()} | ||
</MobileMenuSub> | ||
{suffix} | ||
</> | ||
); | ||
}; |
Oops, something went wrong.