fix: page info styles (#5910)

fix inconsistent styles compared to the one defined in figma
fix https://github.com/toeverything/AFFiNE/issues/5904
fix https://github.com/toeverything/AFFiNE/issues/5903
This commit is contained in:
Peng Xiao
2024-02-26 14:11:24 +00:00
parent 6ccc4f1501
commit 2295685590
10 changed files with 194 additions and 53 deletions

View File

@@ -1,12 +1,10 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { atom } from 'jotai';
import { createContext } from 'react'; import { createContext } from 'react';
import type { PagePropertiesManager } from './page-properties-manager'; import type { PagePropertiesManager } from './page-properties-manager';
// @ts-expect-error this should always be set // @ts-expect-error this should always be set
export const managerContext = createContext<PagePropertiesManager>(); export const managerContext = createContext<PagePropertiesManager>();
export const pageInfoCollapsedAtom = atom(false);
type TagColorHelper<T> = T extends `paletteLine${infer Color}` ? Color : never; type TagColorHelper<T> = T extends `paletteLine${infer Color}` ? Color : never;
type TagColorName = TagColorHelper<Parameters<typeof cssVar>[0]>; type TagColorName = TagColorHelper<Parameters<typeof cssVar>[0]>;

View File

@@ -11,7 +11,13 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { Page, useLiveData, useService, Workspace } from '@toeverything/infra'; import { Page, useLiveData, useService, Workspace } from '@toeverything/infra';
import { noop } from 'lodash-es'; import { noop } from 'lodash-es';
import { type ChangeEventHandler, useCallback, useContext } from 'react'; import {
type ChangeEventHandler,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import { managerContext } from './common'; import { managerContext } from './common';
import * as styles from './styles.css'; import * as styles from './styles.css';
@@ -84,30 +90,85 @@ export const CheckboxValue = ({ property }: PropertyRowValueProps) => {
); );
}; };
export const TextValue = ({ property, meta }: PropertyRowValueProps) => { export const TextValue = ({ property }: PropertyRowValueProps) => {
const manager = useContext(managerContext); const manager = useContext(managerContext);
const [value, setValue] = useState<string>(property.value);
const handleClick = useCallback((e: React.MouseEvent) => { const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
// todo: show edit popup
}, []); }, []);
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useCallback( const handleBlur = useCallback(
e => { (e: React.ChangeEvent<HTMLTextAreaElement>) => {
manager.updateCustomProperty(property.id, { manager.updateCustomProperty(property.id, {
value: e.target.value, value: e.target.value.trim(),
}); });
}, },
[manager, property.id] [manager, property.id]
); );
const handleOnChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
e => {
setValue(e.target.value);
},
[]
);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const isNumber = meta.type === 'number'; useEffect(() => {
setValue(property.value);
}, [property.value]);
return (
<div onClick={handleClick} className={styles.propertyRowValueTextCell}>
<textarea
className={styles.propertyRowValueTextarea}
value={value || ''}
onChange={handleOnChange}
onClick={handleClick}
onBlur={handleBlur}
data-empty={!value}
placeholder={t[
'com.affine.page-properties.property-value-placeholder'
]()}
/>
<div className={styles.propertyRowValueTextareaInvisible}>
{value}
{value?.endsWith('\n') || value === '' ? <br /> : null}
</div>
</div>
);
};
export const NumberValue = ({ property }: PropertyRowValueProps) => {
const manager = useContext(managerContext);
const [value, setValue] = useState(property.value);
const handleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
}, []);
const handleBlur = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
manager.updateCustomProperty(property.id, {
value: e.target.value.trim(),
});
},
[manager, property.id]
);
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useCallback(
e => {
setValue(e.target.value);
},
[]
);
const t = useAFFiNEI18N();
useEffect(() => {
setValue(property.value);
}, [property.value]);
return ( return (
<input <input
type={isNumber ? 'number' : 'text'} className={styles.propertyRowValueNumberCell}
value={property.value || ''} type={'number'}
value={value || ''}
onChange={handleOnChange} onChange={handleOnChange}
onClick={handleClick} onClick={handleClick}
className={styles.propertyRowValueTextCell} onBlur={handleBlur}
data-empty={!property.value} data-empty={!value}
placeholder={t['com.affine.page-properties.property-value-placeholder']()} placeholder={t['com.affine.page-properties.property-value-placeholder']()}
/> />
); );
@@ -152,7 +213,7 @@ export const propertyValueRenderers: Record<
date: DateValue, date: DateValue,
checkbox: CheckboxValue, checkbox: CheckboxValue,
text: TextValue, text: TextValue,
number: TextValue, number: NumberValue,
// todo: fix following // todo: fix following
tags: TagsValue, tags: TagsValue,
progress: TextValue, progress: TextValue,

View File

@@ -7,6 +7,7 @@ export const root = style({
display: 'flex', display: 'flex',
width: '100%', width: '100%',
justifyContent: 'center', justifyContent: 'center',
fontFamily: cssVar('fontFamily'),
vars: { vars: {
[propertyNameCellWidth]: '160px', [propertyNameCellWidth]: '160px',
}, },
@@ -40,6 +41,7 @@ export const tableHeaderInfoRow = style({
color: cssVar('textSecondaryColor'), color: cssVar('textSecondaryColor'),
fontSize: cssVar('fontSm'), fontSize: cssVar('fontSm'),
fontWeight: 500, fontWeight: 500,
minHeight: 34,
}); });
export const tableHeaderSecondaryRow = style({ export const tableHeaderSecondaryRow = style({
@@ -87,7 +89,7 @@ export const tableHeaderTimestamp = style({
}); });
export const tableHeaderDivider = style({ export const tableHeaderDivider = style({
height: '1px', height: '0.5px',
width: '100%', width: '100%',
margin: '8px 0', margin: '8px 0',
backgroundColor: cssVar('dividerColor'), backgroundColor: cssVar('dividerColor'),
@@ -118,7 +120,6 @@ export const addPropertyButton = style({
color: cssVar('textPrimaryColor'), color: cssVar('textPrimaryColor'),
backgroundColor: cssVar('hoverColor'), backgroundColor: cssVar('hoverColor'),
}, },
marginTop: '8px',
}); });
export const collapsedIcon = style({ export const collapsedIcon = style({
@@ -233,6 +234,7 @@ export const propertyRowNameCell = style([
propertyRowCell, propertyRowCell,
{ {
padding: 6, padding: 6,
flexShrink: 0,
color: cssVar('textSecondaryColor'), color: cssVar('textSecondaryColor'),
width: propertyNameCellWidth, width: propertyNameCellWidth,
gap: 6, gap: 6,
@@ -298,13 +300,49 @@ export const propertyRowValueCell = style([
export const propertyRowValueTextCell = style([ export const propertyRowValueTextCell = style([
propertyRowValueCell, propertyRowValueCell,
{ {
':focus': { padding: 0,
position: 'relative',
':focus-within': {
border: `1px solid ${cssVar('blue700')}`, border: `1px solid ${cssVar('blue700')}`,
boxShadow: cssVar('activeShadow'), boxShadow: cssVar('activeShadow'),
}, },
}, },
]); ]);
export const propertyRowValueTextarea = style([
propertyRowValueCell,
{
border: 'none',
padding: '6px 8px',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden',
},
]);
export const propertyRowValueTextareaInvisible = style([
propertyRowValueCell,
{
border: 'none',
padding: '6px 8px',
visibility: 'hidden',
whiteSpace: 'break-spaces',
wordBreak: 'break-all',
overflow: 'hidden',
},
]);
export const propertyRowValueNumberCell = style([
propertyRowValueTextCell,
{
padding: '6px 8px',
},
]);
export const menuHeader = style({ export const menuHeader = style({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
@@ -429,7 +467,7 @@ export const propertyRowTypeItem = style({
export const propertyTypeName = style({ export const propertyTypeName = style({
color: cssVar('textSecondaryColor'), color: cssVar('textSecondaryColor'),
fontSize: cssVar('fontXs'), fontSize: cssVar('fontSm'),
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 4, gap: 4,

View File

@@ -44,7 +44,7 @@ import { SortableContext, useSortable } from '@dnd-kit/sortable';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import clsx from 'clsx'; import clsx from 'clsx';
import { use } from 'foxact/use'; import { use } from 'foxact/use';
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; import { atom, useAtomValue, useSetAtom } from 'jotai';
import type React from 'react'; import type React from 'react';
import { import {
type CSSProperties, type CSSProperties,
@@ -60,7 +60,7 @@ import {
} from 'react'; } from 'react';
import { AffinePageReference } from '../reference-link'; import { AffinePageReference } from '../reference-link';
import { managerContext, pageInfoCollapsedAtom } from './common'; import { managerContext } from './common';
import { ConfirmDeletePropertyModal } from './confirm-delete-property-modal'; import { ConfirmDeletePropertyModal } from './confirm-delete-property-modal';
import { import {
getDefaultIconName, getDefaultIconName,
@@ -583,6 +583,8 @@ export const PagePropertyRowNameMenu = ({
interface PagePropertiesTableHeaderProps { interface PagePropertiesTableHeaderProps {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
open: boolean;
onOpenChange: (open: boolean) => void;
} }
// backlinks - #no Updated yyyy-mm-dd // backlinks - #no Updated yyyy-mm-dd
@@ -591,6 +593,8 @@ interface PagePropertiesTableHeaderProps {
export const PagePropertiesTableHeader = ({ export const PagePropertiesTableHeader = ({
className, className,
style, style,
open,
onOpenChange,
}: PagePropertiesTableHeaderProps) => { }: PagePropertiesTableHeaderProps) => {
const manager = useContext(managerContext); const manager = useContext(managerContext);
@@ -630,10 +634,9 @@ export const PagePropertiesTableHeader = ({
); );
}, [manager.createDate, manager.updatedDate, t]); }, [manager.createDate, manager.updatedDate, t]);
const [collapsed, setCollapsed] = useAtom(pageInfoCollapsedAtom);
const handleCollapse = useCallback(() => { const handleCollapse = useCallback(() => {
setCollapsed(prev => !prev); onOpenChange(!open);
}, [setCollapsed]); }, [onOpenChange, open]);
const properties = manager.getOrderedCustomProperties(); const properties = manager.getOrderedCustomProperties();
@@ -652,7 +655,7 @@ export const PagePropertiesTableHeader = ({
</div> </div>
<Divider /> <Divider />
<div className={styles.tableHeaderSecondaryRow}> <div className={styles.tableHeaderSecondaryRow}>
<div className={clsx(collapsed ? styles.pageInfoDimmed : null)}> <div className={clsx(!open ? styles.pageInfoDimmed : null)}>
{t['com.affine.page-properties.page-info']()} {t['com.affine.page-properties.page-info']()}
</div> </div>
{properties.length === 0 || manager.readonly ? null : ( {properties.length === 0 || manager.readonly ? null : (
@@ -668,10 +671,11 @@ export const PagePropertiesTableHeader = ({
<Collapsible.Trigger asChild role="button" onClick={handleCollapse}> <Collapsible.Trigger asChild role="button" onClick={handleCollapse}>
<IconButton <IconButton
type="plain" type="plain"
data-testid="page-info-collapse"
icon={ icon={
<ToggleExpandIcon <ToggleExpandIcon
className={styles.collapsedIcon} className={styles.collapsedIcon}
data-collapsed={collapsed !== false} data-collapsed={!open}
/> />
} }
/> />
@@ -782,24 +786,27 @@ export const PagePropertiesTableBody = ({
style={style} style={style}
> >
<PageTagsRow /> <PageTagsRow />
<div className={styles.tableBodySortable}> <SortableProperties>
<SortableProperties> {properties =>
{properties => properties.length ? (
properties <div className={styles.tableBodySortable}>
.filter( {properties
property => .filter(
manager.isPropertyRequired(property.id) || property =>
(property.visibility !== 'hide' && manager.isPropertyRequired(property.id) ||
!( (property.visibility !== 'hide' &&
property.visibility === 'hide-if-empty' && !property.value !(
)) property.visibility === 'hide-if-empty' &&
) !property.value
.map(property => ( ))
<PagePropertyRow key={property.id} property={property} /> )
)) .map(property => (
} <PagePropertyRow key={property.id} property={property} />
</SortableProperties> ))}
</div> </div>
) : null
}
</SortableProperties>
{manager.readonly ? null : <PagePropertiesAddProperty />} {manager.readonly ? null : <PagePropertiesAddProperty />}
<Divider /> <Divider />
</Collapsible.Content> </Collapsible.Content>
@@ -1004,12 +1011,16 @@ export const PagePropertiesAddProperty = () => {
const PagePropertiesTableInner = () => { const PagePropertiesTableInner = () => {
const manager = useContext(managerContext); const manager = useContext(managerContext);
const collapsed = useAtomValue(pageInfoCollapsedAtom); const [expanded, setExpanded] = useState(false);
use(manager.workspace.blockSuiteWorkspace.doc.whenSynced); use(manager.workspace.blockSuiteWorkspace.doc.whenSynced);
return ( return (
<div className={styles.root}> <div className={styles.root}>
<Collapsible.Root open={!collapsed} className={styles.rootCentered}> <Collapsible.Root
<PagePropertiesTableHeader /> open={expanded}
onOpenChange={setExpanded}
className={styles.rootCentered}
>
<PagePropertiesTableHeader open={expanded} onOpenChange={setExpanded} />
<PagePropertiesTableBody /> <PagePropertiesTableBody />
</Collapsible.Root> </Collapsible.Root>
</div> </div>

View File

@@ -1,8 +1,7 @@
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css'; import { globalStyle, style } from '@vanilla-extract/css';
export const tagsInlineEditor = style({ export const tagsInlineEditor = style({
width: '100%',
selectors: { selectors: {
'&[data-empty=true]': { '&[data-empty=true]': {
color: cssVar('placeholderColor'), color: cssVar('placeholderColor'),
@@ -14,7 +13,7 @@ export const tagsEditorRoot = style({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%', width: '100%',
gap: '8px', gap: '12px',
}); });
export const inlineTagsContainer = style({ export const inlineTagsContainer = style({
@@ -70,7 +69,7 @@ export const tagsEditorTagsSelectorHeader = style({
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
padding: '0 8px', padding: '0 8px',
fontSize: '14px', fontSize: cssVar('fontXs'),
fontWeight: 500, fontWeight: 500,
color: cssVar('textSecondaryColor'), color: cssVar('textSecondaryColor'),
}); });
@@ -96,6 +95,19 @@ export const tagSelectorItem = style({
}, },
}); });
export const tagEditIcon = style({
opacity: 0,
selectors: {
[`${tagSelectorItem}:hover &`]: {
opacity: 1,
},
},
});
globalStyle(`${tagEditIcon}[data-state=open]`, {
opacity: 1,
});
export const spacer = style({ export const spacer = style({
flexGrow: 1, flexGrow: 1,
}); });

View File

@@ -292,7 +292,11 @@ export const TagsEditor = ({
<TagItem maxWidth="100%" tag={tag} mode="inline" /> <TagItem maxWidth="100%" tag={tag} mode="inline" />
<div className={styles.spacer} /> <div className={styles.spacer} />
<EditTagMenu tag={tag}> <EditTagMenu tag={tag}>
<IconButton type="plain" icon={<MoreHorizontalIcon />} /> <IconButton
className={styles.tagEditIcon}
type="plain"
icon={<MoreHorizontalIcon />}
/>
</EditTagMenu> </EditTagMenu>
</div> </div>
); );
@@ -319,7 +323,7 @@ export const TagsEditor = ({
</div> </div>
)} )}
</Scrollable.Viewport> </Scrollable.Viewport>
<Scrollable.Scrollbar /> <Scrollable.Scrollbar style={{ transform: 'translateX(6px)' }} />
</Scrollable.Root> </Scrollable.Root>
</div> </div>
</div> </div>

View File

@@ -82,7 +82,7 @@ export const tagInnerWrapper = style({
export const tagInline = style([ export const tagInline = style([
tagInnerWrapper, tagInnerWrapper,
{ {
fontSize: cssVar('fontXs'), fontSize: cssVar('fontSm'),
borderRadius: '10px', borderRadius: '10px',
columnGap: '4px', columnGap: '4px',
border: `1px solid ${cssVar('borderColor')}`, border: `1px solid ${cssVar('borderColor')}`,

View File

@@ -16,6 +16,7 @@ import {
changePropertyVisibility, changePropertyVisibility,
clickPropertyValue, clickPropertyValue,
closeTagsEditor, closeTagsEditor,
ensurePagePropertiesVisible,
expectTagsVisible, expectTagsVisible,
getPropertyValueLocator, getPropertyValueLocator,
openTagsEditor, openTagsEditor,
@@ -30,6 +31,7 @@ test.beforeEach(async ({ page }) => {
await clickPageModeButton(page); await clickPageModeButton(page);
await clickNewPageButton(page); await clickNewPageButton(page);
await waitForEmptyEditor(page); await waitForEmptyEditor(page);
await ensurePagePropertiesVisible(page);
}); });
test('allow create tag', async ({ page }) => { test('allow create tag', async ({ page }) => {
@@ -48,6 +50,7 @@ test('allow create tag', async ({ page }) => {
test('allow create tag on journals page', async ({ page }) => { test('allow create tag on journals page', async ({ page }) => {
await openJournalsPage(page); await openJournalsPage(page);
await waitForEditorLoad(page); await waitForEditorLoad(page);
await ensurePagePropertiesVisible(page);
await openTagsEditor(page); await openTagsEditor(page);
await searchAndCreateTag(page, 'Test1'); await searchAndCreateTag(page, 'Test1');

View File

@@ -126,6 +126,7 @@ export const createPageWithTag = async (
await clickNewPageButton(page); await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click(); await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('test page'); await getBlockSuiteEditorTitle(page).fill('test page');
await page.getByTestId('page-info-collapse').click();
await page await page
.locator('[data-testid="page-property-row"][data-property="tags"]') .locator('[data-testid="page-property-row"][data-property="tags"]')
.click(); .click();

View File

@@ -6,6 +6,19 @@ export const getPropertyValueLocator = (page: Page, property: string) => {
); );
}; };
export const ensurePagePropertiesVisible = async (page: Page) => {
if (
await page
.getByRole('button', {
name: 'Add property',
})
.isVisible()
) {
return;
}
await page.getByTestId('page-info-collapse').click();
};
export const clickPropertyValue = async (page: Page, property: string) => { export const clickPropertyValue = async (page: Page, property: string) => {
await getPropertyValueLocator(page, property).click(); await getPropertyValueLocator(page, property).click();
}; };