-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(core): new template doc property #9538
base: canary
Are you sure you want to change the base?
Changes from all commits
a26d68a
e8a4a64
6612a19
02b2d03
0905ead
4aa21f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { cssVarV2 } from '@toeverything/theme/v2'; | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
import { container } from './bi-directional-link-panel.css'; | ||
|
||
export const root = style([ | ||
container, | ||
{ | ||
paddingBottom: 6, | ||
display: 'flex', | ||
gap: 8, | ||
alignItems: 'center', | ||
|
||
fontSize: 12, | ||
fontWeight: 400, | ||
lineHeight: '20px', | ||
color: cssVarV2.text.primary, | ||
}, | ||
]); | ||
|
||
export const badges = style({ | ||
display: 'flex', | ||
gap: 12, | ||
alignItems: 'center', | ||
}); | ||
|
||
export const badge = style({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: 4, | ||
padding: '2px 8px', | ||
borderRadius: 40, | ||
backgroundColor: cssVarV2.layer.background.secondary, | ||
cursor: 'pointer', | ||
userSelect: 'none', | ||
position: 'relative', | ||
|
||
':before': { | ||
content: '""', | ||
position: 'absolute', | ||
left: 0, | ||
top: 0, | ||
width: '100%', | ||
height: '100%', | ||
backgroundColor: 'rgba(0,0,0,.04)', | ||
borderRadius: 'inherit', | ||
opacity: 0, | ||
transition: 'opacity 0.2s ease', | ||
}, | ||
|
||
selectors: { | ||
'&:hover:before': { | ||
opacity: 1, | ||
}, | ||
'&[data-active="true"]:before': { | ||
opacity: 1, | ||
}, | ||
}, | ||
}); | ||
|
||
export const badgeIcon = style({ | ||
fontSize: 16, | ||
lineHeight: 0, | ||
}); | ||
|
||
export const badgeText = style({}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { FeatureFlagService } from '@affine/core/modules/feature-flag'; | ||
import { | ||
TemplateDocService, | ||
TemplateListMenu, | ||
} from '@affine/core/modules/template-doc'; | ||
import { useI18n } from '@affine/i18n'; | ||
import type { Blocks } from '@blocksuite/affine/store'; | ||
import { | ||
AiIcon, | ||
EdgelessIcon, | ||
TemplateColoredIcon, | ||
} from '@blocksuite/icons/rc'; | ||
import { useLiveData, useService } from '@toeverything/infra'; | ||
import clsx from 'clsx'; | ||
import { | ||
forwardRef, | ||
type HTMLAttributes, | ||
useEffect, | ||
useMemo, | ||
useState, | ||
} from 'react'; | ||
|
||
import * as styles from './starter-bar.css'; | ||
|
||
const Badge = forwardRef< | ||
HTMLLIElement, | ||
HTMLAttributes<HTMLLIElement> & { | ||
icon: React.ReactNode; | ||
text: string; | ||
active?: boolean; | ||
} | ||
>(function Badge({ icon, text, className, active, ...attrs }, ref) { | ||
return ( | ||
<li | ||
data-active={active} | ||
className={clsx(styles.badge, className)} | ||
ref={ref} | ||
{...attrs} | ||
> | ||
<span className={styles.badgeText}>{text}</span> | ||
<span className={styles.badgeIcon}>{icon}</span> | ||
</li> | ||
); | ||
}); | ||
|
||
const StarterBarNotEmpty = ({ doc }: { doc: Blocks }) => { | ||
const t = useI18n(); | ||
|
||
const templateDocService = useService(TemplateDocService); | ||
const featureFlagService = useService(FeatureFlagService); | ||
|
||
const [templateMenuOpen, setTemplateMenuOpen] = useState(false); | ||
|
||
const isTemplate = useLiveData( | ||
useMemo( | ||
() => templateDocService.list.isTemplate$(doc.id), | ||
[doc.id, templateDocService.list] | ||
) | ||
); | ||
const enableTemplateDoc = useLiveData( | ||
featureFlagService.flags.enable_template_doc.$ | ||
); | ||
|
||
const showAI = false; | ||
const showEdgeless = false; | ||
const showTemplate = !isTemplate && enableTemplateDoc; | ||
|
||
if (!showAI && !showEdgeless && !showTemplate) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div className={styles.root}> | ||
{t['com.affine.page-starter-bar.start']()} | ||
<ul className={styles.badges}> | ||
{showAI ? ( | ||
<Badge | ||
icon={<AiIcon />} | ||
text={t['com.affine.page-starter-bar.ai']()} | ||
/> | ||
) : null} | ||
|
||
{showTemplate ? ( | ||
<TemplateListMenu | ||
target={doc.id} | ||
rootOptions={{ | ||
open: templateMenuOpen, | ||
onOpenChange: setTemplateMenuOpen, | ||
}} | ||
> | ||
<Badge | ||
data-testid="template-docs-badge" | ||
icon={<TemplateColoredIcon />} | ||
text={t['com.affine.page-starter-bar.template']()} | ||
active={templateMenuOpen} | ||
/> | ||
</TemplateListMenu> | ||
) : null} | ||
|
||
{showEdgeless ? ( | ||
<Badge | ||
icon={<EdgelessIcon />} | ||
text={t['com.affine.page-starter-bar.edgeless']()} | ||
/> | ||
) : null} | ||
</ul> | ||
</div> | ||
); | ||
}; | ||
|
||
export const StarterBar = ({ doc }: { doc: Blocks }) => { | ||
const [isEmpty, setIsEmpty] = useState(doc.isEmpty); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems that this will never be empty, because doc has at least one note There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The empty judgment logic has been refactored in #9570 |
||
|
||
useEffect(() => { | ||
const disposable = doc.slots.blockUpdated.on(() => { | ||
const empty = doc.isEmpty; | ||
setIsEmpty(empty); | ||
// once the doc is not empty, stop checking | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if user delete all the content? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is acceptable to display the Starter only when it is empty for the first time; otherwise, any edits need to check whether the document is empty. |
||
if (!empty) disposable.dispose(); | ||
}); | ||
return () => { | ||
disposable.dispose(); | ||
}; | ||
}, [doc]); | ||
|
||
if (!isEmpty) return null; | ||
|
||
return <StarterBarNotEmpty doc={doc} />; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { cssVarV2 } from '@toeverything/theme/v2'; | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const property = style({ | ||
padding: '3px 4px', | ||
display: 'flex', | ||
alignItems: 'center', | ||
}); | ||
|
||
export const checkbox = style({ | ||
fontSize: 24, | ||
color: cssVarV2.icon.primary, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { Checkbox, PropertyValue } from '@affine/component'; | ||
import { DocService } from '@affine/core/modules/doc'; | ||
import { useLiveData, useService } from '@toeverything/infra'; | ||
import { type ChangeEvent, useCallback } from 'react'; | ||
|
||
import * as styles from './template.css'; | ||
|
||
export const TemplateValue = () => { | ||
const docService = useService(DocService); | ||
|
||
const isTemplate = useLiveData( | ||
docService.doc.record.properties$.selector(p => p.isTemplate) | ||
); | ||
|
||
const onChange = useCallback( | ||
(e: ChangeEvent<HTMLInputElement>) => { | ||
const value = e.target.checked; | ||
docService.doc.record.setProperty('isTemplate', value); | ||
}, | ||
[docService.doc.record] | ||
); | ||
|
||
const toggle = useCallback(() => { | ||
docService.doc.record.setProperty('isTemplate', !isTemplate); | ||
}, [docService.doc.record, isTemplate]); | ||
|
||
return ( | ||
<PropertyValue className={styles.property} onClick={toggle}> | ||
<Checkbox | ||
data-testid="toggle-template-checkbox" | ||
checked={!!isTemplate} | ||
onChange={onChange} | ||
className={styles.checkbox} | ||
/> | ||
</PropertyValue> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May not render this if there is no template docs in the workspace