mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 09:22:38 +03:00
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:
parent
6ccc4f1501
commit
2295685590
@ -1,12 +1,10 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { atom } from 'jotai';
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { PagePropertiesManager } from './page-properties-manager';
|
||||
|
||||
// @ts-expect-error this should always be set
|
||||
export const managerContext = createContext<PagePropertiesManager>();
|
||||
export const pageInfoCollapsedAtom = atom(false);
|
||||
|
||||
type TagColorHelper<T> = T extends `paletteLine${infer Color}` ? Color : never;
|
||||
type TagColorName = TagColorHelper<Parameters<typeof cssVar>[0]>;
|
||||
|
@ -11,7 +11,13 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { Page, useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
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 * 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 [value, setValue] = useState<string>(property.value);
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
// todo: show edit popup
|
||||
}, []);
|
||||
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
e => {
|
||||
const handleBlur = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
manager.updateCustomProperty(property.id, {
|
||||
value: e.target.value,
|
||||
value: e.target.value.trim(),
|
||||
});
|
||||
},
|
||||
[manager, property.id]
|
||||
);
|
||||
const handleOnChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||
e => {
|
||||
setValue(e.target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
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 (
|
||||
<input
|
||||
type={isNumber ? 'number' : 'text'}
|
||||
value={property.value || ''}
|
||||
className={styles.propertyRowValueNumberCell}
|
||||
type={'number'}
|
||||
value={value || ''}
|
||||
onChange={handleOnChange}
|
||||
onClick={handleClick}
|
||||
className={styles.propertyRowValueTextCell}
|
||||
data-empty={!property.value}
|
||||
onBlur={handleBlur}
|
||||
data-empty={!value}
|
||||
placeholder={t['com.affine.page-properties.property-value-placeholder']()}
|
||||
/>
|
||||
);
|
||||
@ -152,7 +213,7 @@ export const propertyValueRenderers: Record<
|
||||
date: DateValue,
|
||||
checkbox: CheckboxValue,
|
||||
text: TextValue,
|
||||
number: TextValue,
|
||||
number: NumberValue,
|
||||
// todo: fix following
|
||||
tags: TagsValue,
|
||||
progress: TextValue,
|
||||
|
@ -7,6 +7,7 @@ export const root = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
fontFamily: cssVar('fontFamily'),
|
||||
vars: {
|
||||
[propertyNameCellWidth]: '160px',
|
||||
},
|
||||
@ -40,6 +41,7 @@ export const tableHeaderInfoRow = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
minHeight: 34,
|
||||
});
|
||||
|
||||
export const tableHeaderSecondaryRow = style({
|
||||
@ -87,7 +89,7 @@ export const tableHeaderTimestamp = style({
|
||||
});
|
||||
|
||||
export const tableHeaderDivider = style({
|
||||
height: '1px',
|
||||
height: '0.5px',
|
||||
width: '100%',
|
||||
margin: '8px 0',
|
||||
backgroundColor: cssVar('dividerColor'),
|
||||
@ -118,7 +120,6 @@ export const addPropertyButton = style({
|
||||
color: cssVar('textPrimaryColor'),
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
marginTop: '8px',
|
||||
});
|
||||
|
||||
export const collapsedIcon = style({
|
||||
@ -233,6 +234,7 @@ export const propertyRowNameCell = style([
|
||||
propertyRowCell,
|
||||
{
|
||||
padding: 6,
|
||||
flexShrink: 0,
|
||||
color: cssVar('textSecondaryColor'),
|
||||
width: propertyNameCellWidth,
|
||||
gap: 6,
|
||||
@ -298,13 +300,49 @@ export const propertyRowValueCell = style([
|
||||
export const propertyRowValueTextCell = style([
|
||||
propertyRowValueCell,
|
||||
{
|
||||
':focus': {
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
':focus-within': {
|
||||
border: `1px solid ${cssVar('blue700')}`,
|
||||
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({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
@ -429,7 +467,7 @@ export const propertyRowTypeItem = style({
|
||||
|
||||
export const propertyTypeName = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
|
@ -44,7 +44,7 @@ import { SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import clsx from 'clsx';
|
||||
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 CSSProperties,
|
||||
@ -60,7 +60,7 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import { AffinePageReference } from '../reference-link';
|
||||
import { managerContext, pageInfoCollapsedAtom } from './common';
|
||||
import { managerContext } from './common';
|
||||
import { ConfirmDeletePropertyModal } from './confirm-delete-property-modal';
|
||||
import {
|
||||
getDefaultIconName,
|
||||
@ -583,6 +583,8 @@ export const PagePropertyRowNameMenu = ({
|
||||
interface PagePropertiesTableHeaderProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
// backlinks - #no Updated yyyy-mm-dd
|
||||
@ -591,6 +593,8 @@ interface PagePropertiesTableHeaderProps {
|
||||
export const PagePropertiesTableHeader = ({
|
||||
className,
|
||||
style,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: PagePropertiesTableHeaderProps) => {
|
||||
const manager = useContext(managerContext);
|
||||
|
||||
@ -630,10 +634,9 @@ export const PagePropertiesTableHeader = ({
|
||||
);
|
||||
}, [manager.createDate, manager.updatedDate, t]);
|
||||
|
||||
const [collapsed, setCollapsed] = useAtom(pageInfoCollapsedAtom);
|
||||
const handleCollapse = useCallback(() => {
|
||||
setCollapsed(prev => !prev);
|
||||
}, [setCollapsed]);
|
||||
onOpenChange(!open);
|
||||
}, [onOpenChange, open]);
|
||||
|
||||
const properties = manager.getOrderedCustomProperties();
|
||||
|
||||
@ -652,7 +655,7 @@ export const PagePropertiesTableHeader = ({
|
||||
</div>
|
||||
<Divider />
|
||||
<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']()}
|
||||
</div>
|
||||
{properties.length === 0 || manager.readonly ? null : (
|
||||
@ -668,10 +671,11 @@ export const PagePropertiesTableHeader = ({
|
||||
<Collapsible.Trigger asChild role="button" onClick={handleCollapse}>
|
||||
<IconButton
|
||||
type="plain"
|
||||
data-testid="page-info-collapse"
|
||||
icon={
|
||||
<ToggleExpandIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed !== false}
|
||||
data-collapsed={!open}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@ -782,24 +786,27 @@ export const PagePropertiesTableBody = ({
|
||||
style={style}
|
||||
>
|
||||
<PageTagsRow />
|
||||
<div className={styles.tableBodySortable}>
|
||||
<SortableProperties>
|
||||
{properties =>
|
||||
properties
|
||||
.filter(
|
||||
property =>
|
||||
manager.isPropertyRequired(property.id) ||
|
||||
(property.visibility !== 'hide' &&
|
||||
!(
|
||||
property.visibility === 'hide-if-empty' && !property.value
|
||||
))
|
||||
)
|
||||
.map(property => (
|
||||
<PagePropertyRow key={property.id} property={property} />
|
||||
))
|
||||
}
|
||||
</SortableProperties>
|
||||
</div>
|
||||
<SortableProperties>
|
||||
{properties =>
|
||||
properties.length ? (
|
||||
<div className={styles.tableBodySortable}>
|
||||
{properties
|
||||
.filter(
|
||||
property =>
|
||||
manager.isPropertyRequired(property.id) ||
|
||||
(property.visibility !== 'hide' &&
|
||||
!(
|
||||
property.visibility === 'hide-if-empty' &&
|
||||
!property.value
|
||||
))
|
||||
)
|
||||
.map(property => (
|
||||
<PagePropertyRow key={property.id} property={property} />
|
||||
))}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</SortableProperties>
|
||||
{manager.readonly ? null : <PagePropertiesAddProperty />}
|
||||
<Divider />
|
||||
</Collapsible.Content>
|
||||
@ -1004,12 +1011,16 @@ export const PagePropertiesAddProperty = () => {
|
||||
|
||||
const PagePropertiesTableInner = () => {
|
||||
const manager = useContext(managerContext);
|
||||
const collapsed = useAtomValue(pageInfoCollapsedAtom);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
use(manager.workspace.blockSuiteWorkspace.doc.whenSynced);
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Collapsible.Root open={!collapsed} className={styles.rootCentered}>
|
||||
<PagePropertiesTableHeader />
|
||||
<Collapsible.Root
|
||||
open={expanded}
|
||||
onOpenChange={setExpanded}
|
||||
className={styles.rootCentered}
|
||||
>
|
||||
<PagePropertiesTableHeader open={expanded} onOpenChange={setExpanded} />
|
||||
<PagePropertiesTableBody />
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const tagsInlineEditor = style({
|
||||
width: '100%',
|
||||
selectors: {
|
||||
'&[data-empty=true]': {
|
||||
color: cssVar('placeholderColor'),
|
||||
@ -14,7 +13,7 @@ export const tagsEditorRoot = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '8px',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const inlineTagsContainer = style({
|
||||
@ -70,7 +69,7 @@ export const tagsEditorTagsSelectorHeader = style({
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
fontSize: '14px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 500,
|
||||
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({
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
@ -292,7 +292,11 @@ export const TagsEditor = ({
|
||||
<TagItem maxWidth="100%" tag={tag} mode="inline" />
|
||||
<div className={styles.spacer} />
|
||||
<EditTagMenu tag={tag}>
|
||||
<IconButton type="plain" icon={<MoreHorizontalIcon />} />
|
||||
<IconButton
|
||||
className={styles.tagEditIcon}
|
||||
type="plain"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
/>
|
||||
</EditTagMenu>
|
||||
</div>
|
||||
);
|
||||
@ -319,7 +323,7 @@ export const TagsEditor = ({
|
||||
</div>
|
||||
)}
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
<Scrollable.Scrollbar style={{ transform: 'translateX(6px)' }} />
|
||||
</Scrollable.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -82,7 +82,7 @@ export const tagInnerWrapper = style({
|
||||
export const tagInline = style([
|
||||
tagInnerWrapper,
|
||||
{
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
borderRadius: '10px',
|
||||
columnGap: '4px',
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
changePropertyVisibility,
|
||||
clickPropertyValue,
|
||||
closeTagsEditor,
|
||||
ensurePagePropertiesVisible,
|
||||
expectTagsVisible,
|
||||
getPropertyValueLocator,
|
||||
openTagsEditor,
|
||||
@ -30,6 +31,7 @@ test.beforeEach(async ({ page }) => {
|
||||
await clickPageModeButton(page);
|
||||
await clickNewPageButton(page);
|
||||
await waitForEmptyEditor(page);
|
||||
await ensurePagePropertiesVisible(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 }) => {
|
||||
await openJournalsPage(page);
|
||||
await waitForEditorLoad(page);
|
||||
await ensurePagePropertiesVisible(page);
|
||||
|
||||
await openTagsEditor(page);
|
||||
await searchAndCreateTag(page, 'Test1');
|
||||
|
@ -126,6 +126,7 @@ export const createPageWithTag = async (
|
||||
await clickNewPageButton(page);
|
||||
await getBlockSuiteEditorTitle(page).click();
|
||||
await getBlockSuiteEditorTitle(page).fill('test page');
|
||||
await page.getByTestId('page-info-collapse').click();
|
||||
await page
|
||||
.locator('[data-testid="page-property-row"][data-property="tags"]')
|
||||
.click();
|
||||
|
@ -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) => {
|
||||
await getPropertyValueLocator(page, property).click();
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user