mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08:00
fix(mobile): doc property styles (#8760)
fix AF-1582 fix AF-1671 - mobile doc info dialog styles - added ConfigModal for editing property values in modal, including: - workspace properties: text, number, tags - db properties: text, number, label, link
This commit is contained in:
@@ -162,6 +162,7 @@ export const headerNavToday = style([
|
|||||||
padding: '0 4px',
|
padding: '0 4px',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
color: cssVar('iconColor'),
|
color: cssVar('iconColor'),
|
||||||
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -192,6 +193,7 @@ export const monthViewBodyCell = style([
|
|||||||
height: '28px',
|
height: '28px',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const monthViewBodyCellInner = style([
|
export const monthViewBodyCellInner = style([
|
||||||
interactive,
|
interactive,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
import { RowInput } from './row-input';
|
import { RowInput } from './row-input';
|
||||||
import { input, inputWrapper } from './style.css';
|
import { input, inputWrapper, mobileInputWrapper } from './style.css';
|
||||||
|
|
||||||
export type InputProps = {
|
export type InputProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -50,19 +50,23 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(inputWrapper, className, {
|
className={clsx(
|
||||||
// status
|
BUILD_CONFIG.isMobileEdition ? mobileInputWrapper : inputWrapper,
|
||||||
disabled: disabled,
|
className,
|
||||||
'no-border': noBorder,
|
{
|
||||||
// color
|
// status
|
||||||
error: status === 'error',
|
disabled: disabled,
|
||||||
success: status === 'success',
|
'no-border': noBorder,
|
||||||
warning: status === 'warning',
|
// color
|
||||||
default: status === 'default',
|
error: status === 'error',
|
||||||
// size
|
success: status === 'success',
|
||||||
large: size === 'large',
|
warning: status === 'warning',
|
||||||
'extra-large': size === 'extraLarge',
|
default: status === 'default',
|
||||||
})}
|
// size
|
||||||
|
large: size === 'large',
|
||||||
|
'extra-large': size === 'extraLarge',
|
||||||
|
}
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ export const inputWrapper = style({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobileInputWrapper = style([
|
||||||
|
inputWrapper,
|
||||||
|
{
|
||||||
|
height: 30,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const input = style({
|
export const input = style({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '0',
|
width: '0',
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export const MobileMenu = ({
|
|||||||
className={styles.menuContent}
|
className={styles.menuContent}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
data-testid="mobile-menu-back-button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
className={styles.backButton}
|
className={styles.backButton}
|
||||||
prefix={<ArrowLeftSmallIcon />}
|
prefix={<ArrowLeftSmallIcon />}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const menuContent = style({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
padding: '13px 0px 13px 0px',
|
padding: '13px 0px 13px 0px',
|
||||||
|
maxHeight: 'calc(100dvh - 32px)',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mobileMenuItem = style({
|
export const mobileMenuItem = style({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const MobileMenuSub = ({
|
|||||||
subContentOptions={contentOptions}
|
subContentOptions={contentOptions}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className={className} {...otherTriggerOptions}>
|
<div role="menuitem" className={className} {...otherTriggerOptions}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</MobileMenuSubRaw>
|
</MobileMenuSubRaw>
|
||||||
|
|||||||
@@ -131,8 +131,8 @@ export const modalContent = style({
|
|||||||
width: widthVar,
|
width: widthVar,
|
||||||
height: heightVar,
|
height: heightVar,
|
||||||
minHeight: minHeightVar,
|
minHeight: minHeightVar,
|
||||||
maxHeight: 'calc(100vh - 32px)',
|
maxHeight: 'calc(100dvh - 32px)',
|
||||||
maxWidth: 'calc(100vw - 20px)',
|
maxWidth: 'calc(100dvw - 20px)',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
fontSize: cssVar('fontBase'),
|
fontSize: cssVar('fontBase'),
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const root = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
width: 240,
|
width: '100%',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
vars: {
|
vars: {
|
||||||
[progressHeight]: '10px',
|
[progressHeight]: '10px',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const propertyRoot = style({
|
|||||||
minHeight: 32,
|
minHeight: 32,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
padding: '2px 0px 2px 2px',
|
padding: '2px 0px 2px 2px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[draggable="true"]': {
|
'&[draggable="true"]': {
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
@@ -170,7 +171,7 @@ export const section = style({
|
|||||||
export const sectionHeader = style({
|
export const sectionHeader = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 20,
|
||||||
padding: '4px 6px',
|
padding: '4px 6px',
|
||||||
minHeight: 30,
|
minHeight: 30,
|
||||||
});
|
});
|
||||||
@@ -180,6 +181,7 @@ export const sectionHeaderTrigger = style({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sectionHeaderIcon = style({
|
export const sectionHeaderIcon = style({
|
||||||
@@ -195,6 +197,7 @@ export const sectionHeaderName = style({
|
|||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-collapsed="true"]': {
|
'&[data-collapsed="true"]': {
|
||||||
color: cssVarV2('text/secondary'),
|
color: cssVarV2('text/secondary'),
|
||||||
|
|||||||
@@ -364,7 +364,9 @@ export const PropertyValue = forwardRef<
|
|||||||
className={clsx(styles.propertyValueContainer, className)}
|
className={clsx(styles.propertyValueContainer, className)}
|
||||||
data-readonly={readonly ? 'true' : 'false'}
|
data-readonly={readonly ? 'true' : 'false'}
|
||||||
data-empty={isEmpty ? 'true' : 'false'}
|
data-empty={isEmpty ? 'true' : 'false'}
|
||||||
data-hoverable={hoverable ? 'true' : 'false'}
|
data-hoverable={
|
||||||
|
hoverable && !BUILD_CONFIG.isMobileEdition ? 'true' : 'false'
|
||||||
|
}
|
||||||
data-property-value
|
data-property-value
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import { DeleteIcon, TagsIcon } from '@blocksuite/icons/rc';
|
|
||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import Input from '../input';
|
|
||||||
import { Menu, MenuItem, type MenuProps, MenuSeparator } from '../menu';
|
|
||||||
import { Scrollable } from '../scrollbar';
|
|
||||||
import * as styles from './tag-edit-menu.css';
|
|
||||||
import type { TagColor, TagLike } from './types';
|
|
||||||
|
|
||||||
type TagEditMenuProps = PropsWithChildren<{
|
|
||||||
onTagDelete: (tagId: string) => void;
|
|
||||||
colors: TagColor[];
|
|
||||||
tag: TagLike;
|
|
||||||
onTagChange: (property: keyof TagLike, value: string) => void;
|
|
||||||
jumpToTag?: (tagId: string) => void;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export const TagEditMenu = ({
|
|
||||||
tag,
|
|
||||||
onTagDelete,
|
|
||||||
children,
|
|
||||||
jumpToTag,
|
|
||||||
colors,
|
|
||||||
onTagChange,
|
|
||||||
}: TagEditMenuProps) => {
|
|
||||||
const t = useI18n();
|
|
||||||
|
|
||||||
const menuProps = useMemo(() => {
|
|
||||||
const updateTagName = (name: string) => {
|
|
||||||
if (name.trim() === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onTagChange('value', name);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentOptions: {
|
|
||||||
onClick(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
items: (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
defaultValue={tag.value}
|
|
||||||
onBlur={e => {
|
|
||||||
updateTagName(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
updateTagName(e.currentTarget.value);
|
|
||||||
}
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
placeholder={t['Untitled']()}
|
|
||||||
/>
|
|
||||||
<MenuSeparator />
|
|
||||||
<MenuItem
|
|
||||||
prefixIcon={<DeleteIcon />}
|
|
||||||
type="danger"
|
|
||||||
onClick={() => {
|
|
||||||
tag?.id ? onTagDelete(tag.id) : null;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t['Delete']()}
|
|
||||||
</MenuItem>
|
|
||||||
{jumpToTag ? (
|
|
||||||
<MenuItem
|
|
||||||
prefixIcon={<TagsIcon />}
|
|
||||||
onClick={() => {
|
|
||||||
jumpToTag(tag.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t['com.affine.page-properties.tags.open-tags-page']()}
|
|
||||||
</MenuItem>
|
|
||||||
) : null}
|
|
||||||
<MenuSeparator />
|
|
||||||
<Scrollable.Root>
|
|
||||||
<Scrollable.Viewport className={styles.menuItemList}>
|
|
||||||
{colors.map(({ name, value: color }, i) => (
|
|
||||||
<MenuItem
|
|
||||||
key={i}
|
|
||||||
checked={tag.color === color}
|
|
||||||
prefixIcon={
|
|
||||||
<div key={i} className={styles.tagColorIconWrapper}>
|
|
||||||
<div
|
|
||||||
className={styles.tagColorIcon}
|
|
||||||
style={{
|
|
||||||
backgroundColor: color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
onTagChange('color', color);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
<Scrollable.Scrollbar className={styles.menuItemListScrollbar} />
|
|
||||||
</Scrollable.Viewport>
|
|
||||||
</Scrollable.Root>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
} satisfies Partial<MenuProps>;
|
|
||||||
}, [tag, t, jumpToTag, colors, onTagChange, onTagDelete]);
|
|
||||||
|
|
||||||
return <Menu {...menuProps}>{children}</Menu>;
|
|
||||||
};
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import type { Meta, StoryFn } from '@storybook/react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { InlineTagList } from './inline-tag-list';
|
|
||||||
import { TagItem } from './tag';
|
|
||||||
import { TagEditMenu } from './tag-edit-menu';
|
|
||||||
import { TagsInlineEditor } from './tags-editor';
|
|
||||||
import type { TagColor, TagLike } from './types';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'UI/Tags',
|
|
||||||
} satisfies Meta;
|
|
||||||
|
|
||||||
const tags: TagLike[] = [
|
|
||||||
{ id: '1', value: 'tag', color: 'red' },
|
|
||||||
{ id: '2', value: 'tag2', color: 'blue' },
|
|
||||||
{ id: '3', value: 'tag3', color: 'green' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const tagColors: TagColor[] = [
|
|
||||||
{ id: '1', value: 'red', name: 'Red' },
|
|
||||||
{ id: '2', value: 'blue', name: 'Blue' },
|
|
||||||
{ id: '3', value: 'green', name: 'Green' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Tags: StoryFn = () => {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
|
||||||
<TagItem
|
|
||||||
tag={{ id: '1', value: 'tag', color: 'red' }}
|
|
||||||
mode="inline-tag"
|
|
||||||
focused
|
|
||||||
onRemoved={() => {
|
|
||||||
console.log('removed');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TagItem
|
|
||||||
tag={{ id: '2', value: 'tag2', color: 'blue' }}
|
|
||||||
mode="inline-tag"
|
|
||||||
/>
|
|
||||||
<TagItem
|
|
||||||
tag={{ id: '3', value: 'tag3', color: '#DCFDD7' }}
|
|
||||||
mode="db-label"
|
|
||||||
/>
|
|
||||||
<TagItem
|
|
||||||
tag={{ id: '3', value: 'tag5', color: '#DCFDD7' }}
|
|
||||||
mode="db-label"
|
|
||||||
focused
|
|
||||||
onRemoved={() => {
|
|
||||||
console.log('removed');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TagItem tag={{ id: '1', value: 'tag', color: 'red' }} mode="list-tag" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InlineTagListStory: StoryFn = () => {
|
|
||||||
return <InlineTagList tagMode="inline-tag" tags={tags} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TagEditMenuStory: StoryFn = () => {
|
|
||||||
return (
|
|
||||||
<TagEditMenu
|
|
||||||
tag={tags[0]}
|
|
||||||
colors={tagColors}
|
|
||||||
onTagChange={() => {}}
|
|
||||||
onTagDelete={() => {}}
|
|
||||||
jumpToTag={() => {}}
|
|
||||||
>
|
|
||||||
<div>Trigger Edit Tag Menu</div>
|
|
||||||
</TagEditMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TagsInlineEditorStory: StoryFn = () => {
|
|
||||||
const [options, setOptions] = useState<TagLike[]>(tags);
|
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>(
|
|
||||||
options.slice(0, 1).map(item => item.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TagsInlineEditor
|
|
||||||
tags={options}
|
|
||||||
tagMode="db-label"
|
|
||||||
selectedTags={selectedTags}
|
|
||||||
onCreateTag={(name, color) => {
|
|
||||||
const newTag = {
|
|
||||||
id: (options.at(-1)!.id ?? 0) + 1,
|
|
||||||
value: name,
|
|
||||||
color,
|
|
||||||
};
|
|
||||||
setOptions(prev => [...prev, newTag]);
|
|
||||||
return newTag;
|
|
||||||
}}
|
|
||||||
tagColors={tagColors}
|
|
||||||
onTagChange={(id, property, value) => {
|
|
||||||
setOptions(prev => {
|
|
||||||
const index = prev.findIndex(item => item.id === id);
|
|
||||||
if (index === -1) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
return options.toSpliced(index, 1, {
|
|
||||||
...options[index],
|
|
||||||
[property]: value,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onDeleteTag={tagId => {
|
|
||||||
setOptions(prev => prev.filter(item => item.id !== tagId));
|
|
||||||
}}
|
|
||||||
onSelectTag={tagId => {
|
|
||||||
setSelectedTags(prev => [...prev, tagId]);
|
|
||||||
}}
|
|
||||||
onDeselectTag={tagId => {
|
|
||||||
setSelectedTags(prev => prev.filter(id => id !== tagId));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -280,12 +280,15 @@ export const BlocksuiteDocEditor = forwardRef<
|
|||||||
<BlocksuiteEditorJournalDocTitle page={page} />
|
<BlocksuiteEditorJournalDocTitle page={page} />
|
||||||
)}
|
)}
|
||||||
{!shared && displayDocInfo ? (
|
{!shared && displayDocInfo ? (
|
||||||
<DocPropertiesTable
|
<div className={styles.docPropertiesTableContainer}>
|
||||||
onDatabasePropertyChange={onDatabasePropertyChange}
|
<DocPropertiesTable
|
||||||
onPropertyChange={onPropertyChange}
|
className={styles.docPropertiesTable}
|
||||||
onPropertyAdded={onPropertyAdded}
|
onDatabasePropertyChange={onDatabasePropertyChange}
|
||||||
defaultOpenProperty={defaultOpenProperty}
|
onPropertyChange={onPropertyChange}
|
||||||
/>
|
onPropertyAdded={onPropertyAdded}
|
||||||
|
defaultOpenProperty={defaultOpenProperty}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<adapted.DocEditor
|
<adapted.DocEditor
|
||||||
className={styles.docContainer}
|
className={styles.docContainer}
|
||||||
|
|||||||
@@ -60,3 +60,23 @@ export const pageReferenceIcon = style({
|
|||||||
fontSize: '1.1em',
|
fontSize: '1.1em',
|
||||||
transform: 'translate(2px, -1px)',
|
transform: 'translate(2px, -1px)',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const docPropertiesTableContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const docPropertiesTable = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: cssVar('editorWidth'),
|
||||||
|
padding: `0 ${cssVar('editorSidePadding', '24px')}`,
|
||||||
|
'@container': {
|
||||||
|
[`viewport (width <= 640px)`]: {
|
||||||
|
padding: '0 16px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const iconsRow = style({
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
padding: '0 6px',
|
padding: '0 6px',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
|
justifyContent: 'space-between',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const iconButton = style({
|
export const iconButton = style({
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export const propertyRowNamePopupRow = style({
|
|||||||
minWidth: 260,
|
minWidth: 260,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobilePropertyRowNamePopupRow = style([
|
||||||
|
propertyRowNamePopupRow,
|
||||||
|
{
|
||||||
|
padding: '8px 20px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const propertyRowTypeItem = style({
|
export const propertyRowTypeItem = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -25,6 +32,13 @@ export const propertyRowTypeItem = style({
|
|||||||
minWidth: 260,
|
minWidth: 260,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobilePropertyRowTypeItem = style([
|
||||||
|
propertyRowTypeItem,
|
||||||
|
{
|
||||||
|
padding: '8px 20px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const propertyTypeName = style({
|
export const propertyTypeName = style({
|
||||||
color: cssVarV2('text/secondary'),
|
color: cssVarV2('text/secondary'),
|
||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
@@ -40,3 +54,7 @@ export const propertyName = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobileRootWrapper = style({
|
||||||
|
padding: '10px 16px',
|
||||||
|
});
|
||||||
|
|||||||
@@ -121,7 +121,11 @@ export const EditDocPropertyMenuItems = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={styles.propertyRowNamePopupRow}
|
className={
|
||||||
|
BUILD_CONFIG.isMobileEdition
|
||||||
|
? styles.mobilePropertyRowNamePopupRow
|
||||||
|
: styles.propertyRowNamePopupRow
|
||||||
|
}
|
||||||
data-testid="edit-property-menu-item"
|
data-testid="edit-property-menu-item"
|
||||||
>
|
>
|
||||||
<DocPropertyIconSelector
|
<DocPropertyIconSelector
|
||||||
@@ -140,7 +144,13 @@ export const EditDocPropertyMenuItems = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.propertyRowTypeItem}>
|
<div
|
||||||
|
className={
|
||||||
|
BUILD_CONFIG.isMobileEdition
|
||||||
|
? styles.mobilePropertyRowTypeItem
|
||||||
|
: styles.propertyRowTypeItem
|
||||||
|
}
|
||||||
|
>
|
||||||
{t['com.affine.page-properties.create-property.menu.header']()}
|
{t['com.affine.page-properties.create-property.menu.header']()}
|
||||||
<div className={styles.propertyTypeName}>
|
<div className={styles.propertyTypeName}>
|
||||||
<DocPropertyIcon propertyInfo={propertyInfo} />
|
<DocPropertyIcon propertyInfo={propertyInfo} />
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ const propertyNameCellWidth = createVar();
|
|||||||
export const fontSize = createVar();
|
export const fontSize = createVar();
|
||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
justifyContent: 'center',
|
|
||||||
fontFamily: cssVar('fontSansFamily'),
|
fontFamily: cssVar('fontSansFamily'),
|
||||||
paddingBottom: '18px',
|
paddingBottom: '18px',
|
||||||
vars: {
|
vars: {
|
||||||
@@ -24,20 +22,6 @@ export const root = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const rootCentered = style({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 8,
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: cssVar('editorWidth'),
|
|
||||||
padding: `0 ${cssVar('editorSidePadding', '24px')}`,
|
|
||||||
'@container': {
|
|
||||||
[`viewport (width <= 640px)`]: {
|
|
||||||
padding: '0 16px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tableHeader = style({
|
export const tableHeader = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: 30,
|
height: 30,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type DefaultOpenProperty =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface DocPropertiesTableProps {
|
export interface DocPropertiesTableProps {
|
||||||
|
className?: string;
|
||||||
defaultOpenProperty?: DefaultOpenProperty;
|
defaultOpenProperty?: DefaultOpenProperty;
|
||||||
onPropertyAdded?: (property: DocCustomPropertyInfo) => void;
|
onPropertyAdded?: (property: DocCustomPropertyInfo) => void;
|
||||||
onPropertyChange?: (property: DocCustomPropertyInfo, value: unknown) => void;
|
onPropertyChange?: (property: DocCustomPropertyInfo, value: unknown) => void;
|
||||||
@@ -351,16 +352,17 @@ const DocPropertiesTableInner = ({
|
|||||||
onPropertyAdded,
|
onPropertyAdded,
|
||||||
onPropertyChange,
|
onPropertyChange,
|
||||||
onDatabasePropertyChange,
|
onDatabasePropertyChange,
|
||||||
|
className,
|
||||||
}: DocPropertiesTableProps) => {
|
}: DocPropertiesTableProps) => {
|
||||||
const [expanded, setExpanded] = useState(!!defaultOpenProperty);
|
const [expanded, setExpanded] = useState(!!defaultOpenProperty);
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={clsx(styles.root, className)}>
|
||||||
<Collapsible.Root
|
<Collapsible.Root open={expanded} onOpenChange={setExpanded}>
|
||||||
open={expanded}
|
<DocPropertiesTableHeader
|
||||||
onOpenChange={setExpanded}
|
style={{ width: '100%' }}
|
||||||
className={styles.rootCentered}
|
open={expanded}
|
||||||
>
|
onOpenChange={setExpanded}
|
||||||
<DocPropertiesTableHeader open={expanded} onOpenChange={setExpanded} />
|
/>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<DocWorkspacePropertiesTableBody
|
<DocWorkspacePropertiesTableBody
|
||||||
defaultOpen={
|
defaultOpen={
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { TagLike } from '@affine/component/ui/tags';
|
|
||||||
import { TagsInlineEditor as TagsInlineEditorComponent } from '@affine/component/ui/tags';
|
|
||||||
import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag';
|
import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
import track from '@affine/track';
|
import track from '@affine/track';
|
||||||
|
import { TagsIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
LiveData,
|
LiveData,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
@@ -12,6 +12,10 @@ import { useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
|
import {
|
||||||
|
type TagLike,
|
||||||
|
TagsInlineEditor as TagsInlineEditorComponent,
|
||||||
|
} from '../tags';
|
||||||
|
|
||||||
interface TagsEditorProps {
|
interface TagsEditorProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@@ -126,6 +130,8 @@ export const TagsInlineEditor = ({
|
|||||||
[navigator, workspace.workspace.id]
|
[navigator, workspace.workspace.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TagsInlineEditorComponent
|
<TagsInlineEditorComponent
|
||||||
tagMode="inline-tag"
|
tagMode="inline-tag"
|
||||||
@@ -141,6 +147,12 @@ export const TagsInlineEditor = ({
|
|||||||
tagColors={adaptedTagColors}
|
tagColors={adaptedTagColors}
|
||||||
onTagChange={onTagChange}
|
onTagChange={onTagChange}
|
||||||
onDeleteTag={onTagDelete}
|
onDeleteTag={onTagDelete}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<TagsIcon />
|
||||||
|
{t['Tags']()}
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const date = style({
|
|||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
padding: '0 4px',
|
padding: '0 4px',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
':hover': {
|
':hover': {
|
||||||
background: cssVarV2('layer/background/hoverOverlay'),
|
background: cssVarV2('layer/background/hoverOverlay'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
useServiceOptional,
|
useServiceOptional,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './journal.css';
|
import * as styles from './journal.css';
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ export const JournalValue = () => {
|
|||||||
(_: unknown, v: boolean) => {
|
(_: unknown, v: boolean) => {
|
||||||
if (!v) {
|
if (!v) {
|
||||||
journalService.removeJournalDate(doc.id);
|
journalService.removeJournalDate(doc.id);
|
||||||
|
setShowDatePicker(false);
|
||||||
} else {
|
} else {
|
||||||
handleDateSelect(selectedDate);
|
handleDateSelect(selectedDate);
|
||||||
}
|
}
|
||||||
@@ -71,6 +72,10 @@ export const JournalValue = () => {
|
|||||||
|
|
||||||
const handleOpenDuplicate = useCallback(
|
const handleOpenDuplicate = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
|
// todo: open duplicate dialog for mobile
|
||||||
|
if (BUILD_CONFIG.isMobileEdition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
workbench.openSidebar();
|
workbench.openSidebar();
|
||||||
view.activeSidebarTab('journal');
|
view.activeSidebarTab('journal');
|
||||||
@@ -78,12 +83,23 @@ export const JournalValue = () => {
|
|||||||
[view, workbench]
|
[view, workbench]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const propertyRef = useRef<HTMLDivElement>(null);
|
||||||
handleCheck(null, !checked);
|
|
||||||
}, [checked, handleCheck]);
|
const toggle = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (propertyRef.current?.contains(e.target as Node)) {
|
||||||
|
handleCheck(null, !checked);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[checked, handleCheck]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyValue className={styles.property} onClick={toggle}>
|
<PropertyValue
|
||||||
|
ref={propertyRef}
|
||||||
|
className={styles.property}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className={styles.checkbox}
|
className={styles.checkbox}
|
||||||
@@ -113,7 +129,13 @@ export const JournalValue = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div data-testid="date-selector" className={styles.date}>
|
<div
|
||||||
|
data-testid="date-selector"
|
||||||
|
className={styles.date}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
{displayDate}
|
{displayDate}
|
||||||
</div>
|
</div>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const numberPropertyValueInput = style({
|
export const numberPropertyValueInput = style({
|
||||||
@@ -23,3 +24,18 @@ export const numberPropertyValueInput = style({
|
|||||||
export const numberPropertyValueContainer = style({
|
export const numberPropertyValueContainer = style({
|
||||||
padding: '0px',
|
padding: '0px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const numberIcon = style({
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mobileNumberPropertyValueInput = style([
|
||||||
|
numberPropertyValueInput,
|
||||||
|
{
|
||||||
|
selectors: {
|
||||||
|
'input&': {
|
||||||
|
border: `1px solid ${cssVar('blue700')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { NumberIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
type ChangeEventHandler,
|
type ChangeEventHandler,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -7,10 +8,11 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { ConfigModal } from '../../mobile';
|
||||||
import * as styles from './number.css';
|
import * as styles from './number.css';
|
||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
export const NumberValue = ({ value, onChange }: PropertyValueProps) => {
|
const DesktopNumberValue = ({ value, onChange }: PropertyValueProps) => {
|
||||||
const parsedValue = isNaN(Number(value)) ? null : value;
|
const parsedValue = isNaN(Number(value)) ? null : value;
|
||||||
const [tempValue, setTempValue] = useState(parsedValue);
|
const [tempValue, setTempValue] = useState(parsedValue);
|
||||||
const handleBlur = useCallback(
|
const handleBlur = useCallback(
|
||||||
@@ -48,3 +50,79 @@ export const NumberValue = ({ value, onChange }: PropertyValueProps) => {
|
|||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MobileNumberValue = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
propertyInfo,
|
||||||
|
}: PropertyValueProps) => {
|
||||||
|
const parsedValue = isNaN(Number(value)) ? null : value;
|
||||||
|
const [tempValue, setTempValue] = useState(parsedValue);
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(e.target.value.trim());
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||||
|
e => {
|
||||||
|
setTempValue(e.target.value.trim());
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const t = useI18n();
|
||||||
|
useEffect(() => {
|
||||||
|
setTempValue(parsedValue);
|
||||||
|
}, [parsedValue]);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
onChange(tempValue);
|
||||||
|
}, [onChange, tempValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PropertyValue
|
||||||
|
className={styles.numberPropertyValueContainer}
|
||||||
|
isEmpty={!parsedValue}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.mobileNumberPropertyValueInput}>
|
||||||
|
{value ||
|
||||||
|
t['com.affine.page-properties.property-value-placeholder']()}
|
||||||
|
</div>
|
||||||
|
</PropertyValue>
|
||||||
|
<ConfigModal
|
||||||
|
open={open}
|
||||||
|
variant="popup"
|
||||||
|
onDone={onClose}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<NumberIcon className={styles.numberIcon} />
|
||||||
|
{propertyInfo?.name}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className={styles.mobileNumberPropertyValueInput}
|
||||||
|
type={'number'}
|
||||||
|
value={tempValue || ''}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
data-empty={!tempValue}
|
||||||
|
placeholder={t[
|
||||||
|
'com.affine.page-properties.property-value-placeholder'
|
||||||
|
]()}
|
||||||
|
/>
|
||||||
|
</ConfigModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NumberValue = BUILD_CONFIG.isMobileEdition
|
||||||
|
? MobileNumberValue
|
||||||
|
: DesktopNumberValue;
|
||||||
|
|||||||
@@ -25,6 +25,32 @@ export const textarea = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobileTextareaWrapper = style({
|
||||||
|
position: 'relative',
|
||||||
|
background: cssVarV2('layer/background/primary'),
|
||||||
|
borderRadius: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mobileTextareaBase = {
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: '26px',
|
||||||
|
padding: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mobileTextareaPlain = style([
|
||||||
|
textarea,
|
||||||
|
mobileTextareaBase,
|
||||||
|
{
|
||||||
|
position: 'relative',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '22px',
|
||||||
|
height: 'auto',
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const mobileTextarea = style([textarea, mobileTextareaBase]);
|
||||||
|
|
||||||
export const textPropertyValueContainer = style({
|
export const textPropertyValueContainer = style({
|
||||||
outline: `1px solid transparent`,
|
outline: `1px solid transparent`,
|
||||||
padding: `6px`,
|
padding: `6px`,
|
||||||
@@ -43,4 +69,7 @@ export const textInvisible = style({
|
|||||||
visibility: 'hidden',
|
visibility: 'hidden',
|
||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
|
padding: `6px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobileTextInvisible = style([textInvisible, mobileTextareaBase]);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { TextIcon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
type ChangeEventHandler,
|
type ChangeEventHandler,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -8,10 +9,11 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { ConfigModal } from '../../mobile';
|
||||||
import * as styles from './text.css';
|
import * as styles from './text.css';
|
||||||
import type { PropertyValueProps } from './types';
|
import type { PropertyValueProps } from './types';
|
||||||
|
|
||||||
export const TextValue = ({ value, onChange }: PropertyValueProps) => {
|
const DesktopTextValue = ({ value, onChange }: PropertyValueProps) => {
|
||||||
const [tempValue, setTempValue] = useState<string>(value);
|
const [tempValue, setTempValue] = useState<string>(value);
|
||||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -68,3 +70,94 @@ export const TextValue = ({ value, onChange }: PropertyValueProps) => {
|
|||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MobileTextValue = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
propertyInfo,
|
||||||
|
}: PropertyValueProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const [tempValue, setTempValue] = useState<string>(value);
|
||||||
|
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen(true);
|
||||||
|
}, []);
|
||||||
|
const ref = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(e: FocusEvent) => {
|
||||||
|
onChange((e.currentTarget as HTMLTextAreaElement).value.trim());
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
// use native blur event to get event after unmount
|
||||||
|
// don't use useLayoutEffect here, cause the cleanup function will be called before unmount
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current?.addEventListener('blur', handleBlur);
|
||||||
|
return () => {
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
ref.current?.removeEventListener('blur', handleBlur);
|
||||||
|
};
|
||||||
|
}, [handleBlur]);
|
||||||
|
const handleOnChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
|
||||||
|
e => {
|
||||||
|
setTempValue(e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
onChange(tempValue.trim());
|
||||||
|
}, [onChange, tempValue]);
|
||||||
|
const t = useI18n();
|
||||||
|
useEffect(() => {
|
||||||
|
setTempValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PropertyValue
|
||||||
|
className={styles.textPropertyValueContainer}
|
||||||
|
onClick={handleClick}
|
||||||
|
isEmpty={!value}
|
||||||
|
>
|
||||||
|
<div className={styles.mobileTextareaPlain} data-empty={!tempValue}>
|
||||||
|
{tempValue ||
|
||||||
|
t['com.affine.page-properties.property-value-placeholder']()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ConfigModal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onBack={onClose}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<TextIcon />
|
||||||
|
{propertyInfo?.name}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.mobileTextareaWrapper}>
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
className={styles.mobileTextarea}
|
||||||
|
value={tempValue || ''}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
data-empty={!tempValue}
|
||||||
|
autoFocus
|
||||||
|
placeholder={t[
|
||||||
|
'com.affine.page-properties.property-value-placeholder'
|
||||||
|
]()}
|
||||||
|
/>
|
||||||
|
<div className={styles.mobileTextInvisible}>
|
||||||
|
{tempValue}
|
||||||
|
{tempValue?.endsWith('\n') || !tempValue ? <br /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ConfigModal>
|
||||||
|
</PropertyValue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextValue = BUILD_CONFIG.isMobileWeb
|
||||||
|
? MobileTextValue
|
||||||
|
: DesktopTextValue;
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { Button, Modal } from '@affine/component';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {
|
||||||
|
type CSSProperties,
|
||||||
|
forwardRef,
|
||||||
|
type HTMLProps,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { PageHeader } from '../page-header';
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
interface ConfigModalProps {
|
||||||
|
onBack?: () => void;
|
||||||
|
onDone?: () => void;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
title?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
variant?: 'popup' | 'page';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A modal with a page header for configuring something on mobile (preferable to be fullscreen)
|
||||||
|
*/
|
||||||
|
export const ConfigModal = ({
|
||||||
|
onBack,
|
||||||
|
onDone,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
variant = 'page',
|
||||||
|
}: ConfigModalProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
open={open}
|
||||||
|
fullScreen={variant === 'page'}
|
||||||
|
width="100%"
|
||||||
|
minHeight={0}
|
||||||
|
animation="slideBottom"
|
||||||
|
withoutCloseButton
|
||||||
|
contentOptions={{
|
||||||
|
onClick: e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
className:
|
||||||
|
variant === 'page'
|
||||||
|
? styles.pageModalContent
|
||||||
|
: styles.popupModalContent,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{variant === 'page' ? (
|
||||||
|
<PageHeader
|
||||||
|
back={!!onBack}
|
||||||
|
backAction={onBack}
|
||||||
|
suffix={
|
||||||
|
onDone ? (
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
className={styles.doneButton}
|
||||||
|
variant="plain"
|
||||||
|
onClick={onDone}
|
||||||
|
>
|
||||||
|
{t['Done']()}
|
||||||
|
</Button>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.pageTitle}>{title}</div>
|
||||||
|
</PageHeader>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
variant === 'page' ? styles.pageContent : styles.popupContent
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{variant === 'page' ? null : (
|
||||||
|
<div className={styles.popupTitle}>{title}</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
{variant === 'popup' && onDone ? (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
className={styles.bottomDoneButton}
|
||||||
|
onClick={onDone}
|
||||||
|
>
|
||||||
|
{t['Done']()}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigRow = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
||||||
|
function ConfigRow({ children, className, ...attrs }, ref) {
|
||||||
|
return (
|
||||||
|
<div className={clsx(styles.rowItem, className)} ref={ref} {...attrs}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface SettingGroupProps
|
||||||
|
extends Omit<HTMLProps<HTMLDivElement>, 'title'> {
|
||||||
|
title?: ReactNode;
|
||||||
|
contentClassName?: string;
|
||||||
|
contentStyle?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfigRowGroup = forwardRef<HTMLDivElement, SettingGroupProps>(
|
||||||
|
function ConfigRowGroup(
|
||||||
|
{ children, title, className, contentClassName, contentStyle, ...attrs },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className={clsx(styles.group, className)} ref={ref} {...attrs}>
|
||||||
|
{title ? <div className={styles.groupTitle}>{title}</div> : null}
|
||||||
|
<div
|
||||||
|
className={clsx(styles.groupContent, contentClassName)}
|
||||||
|
style={contentStyle}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ConfigModal.RowGroup = ConfigRowGroup;
|
||||||
|
ConfigModal.Row = ConfigRow;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
bodyEmphasized,
|
||||||
|
footnoteRegular,
|
||||||
|
} from '@toeverything/theme/typography';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const pageModalContent = style({
|
||||||
|
padding: 0,
|
||||||
|
overflowY: 'auto',
|
||||||
|
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const popupModalContent = style({});
|
||||||
|
|
||||||
|
export const pageTitle = style([
|
||||||
|
bodyEmphasized,
|
||||||
|
{
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const popupTitle = style([
|
||||||
|
bodyEmphasized,
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const pageContent = style({
|
||||||
|
padding: '24px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const popupContent = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const doneButton = style([
|
||||||
|
bodyEmphasized,
|
||||||
|
{
|
||||||
|
color: cssVarV2('button/primary'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const bottomDoneButton = style({
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const group = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
width: '100%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const groupTitle = style([
|
||||||
|
footnoteRegular,
|
||||||
|
{
|
||||||
|
color: cssVarV2('text/tertiary'),
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const groupContent = style({
|
||||||
|
background: cssVarV2('layer/background/primary'),
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowItem = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
padding: '8px 4px',
|
||||||
|
});
|
||||||
2
packages/frontend/core/src/components/mobile/index.ts
Normal file
2
packages/frontend/core/src/components/mobile/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './config-modal';
|
||||||
|
export * from './page-header';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Menu } from '@affine/component';
|
import { Menu } from '@affine/component';
|
||||||
import { TagItem as TagItemComponent } from '@affine/component/ui/tags';
|
import { TagItem as TagItemComponent } from '@affine/core/components/tags';
|
||||||
import type { Tag } from '@affine/core/modules/tag';
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { stopPropagation } from '@affine/core/utils';
|
import { stopPropagation } from '@affine/core/utils';
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { globalStyle, style } from '@vanilla-extract/css';
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const tagsInlineEditor = style({
|
export const tagsInlineEditor = style({
|
||||||
@@ -16,6 +17,13 @@ export const tagsEditorRoot = style({
|
|||||||
gap: '12px',
|
gap: '12px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const tagsEditorRootMobile = style([
|
||||||
|
tagsEditorRoot,
|
||||||
|
{
|
||||||
|
gap: 20,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const inlineTagsContainer = style({
|
export const inlineTagsContainer = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '6px',
|
gap: '6px',
|
||||||
@@ -39,6 +47,12 @@ export const tagsEditorSelectedTags = style({
|
|||||||
padding: '10px 12px',
|
padding: '10px 12px',
|
||||||
backgroundColor: cssVar('hoverColor'),
|
backgroundColor: cssVar('hoverColor'),
|
||||||
minHeight: 42,
|
minHeight: 42,
|
||||||
|
selectors: {
|
||||||
|
[`${tagsEditorRootMobile} &`]: {
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: cssVarV2('layer/background/primary'),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const searchInput = style({
|
export const searchInput = style({
|
||||||
@@ -63,6 +77,11 @@ export const tagsEditorTagsSelector = style({
|
|||||||
padding: '0 8px 8px 8px',
|
padding: '0 8px 8px 8px',
|
||||||
maxHeight: '400px',
|
maxHeight: '400px',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
|
selectors: {
|
||||||
|
[`${tagsEditorRootMobile} &`]: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tagsEditorTagsSelectorHeader = style({
|
export const tagsEditorTagsSelectorHeader = style({
|
||||||
@@ -73,6 +92,11 @@ export const tagsEditorTagsSelectorHeader = style({
|
|||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: cssVar('textSecondaryColor'),
|
color: cssVar('textSecondaryColor'),
|
||||||
|
selectors: {
|
||||||
|
[`${tagsEditorRootMobile} &`]: {
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tagSelectorTagsScrollContainer = style({
|
export const tagSelectorTagsScrollContainer = style({
|
||||||
@@ -80,6 +104,14 @@ export const tagSelectorTagsScrollContainer = style({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
maxHeight: '200px',
|
maxHeight: '200px',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
|
selectors: {
|
||||||
|
[`${tagsEditorRootMobile} &`]: {
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: cssVarV2('layer/background/primary'),
|
||||||
|
gap: 0,
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tagSelectorItem = style({
|
export const tagSelectorItem = style({
|
||||||
@@ -95,13 +127,20 @@ export const tagSelectorItem = style({
|
|||||||
'&[data-focused=true]': {
|
'&[data-focused=true]': {
|
||||||
backgroundColor: cssVar('hoverColor'),
|
backgroundColor: cssVar('hoverColor'),
|
||||||
},
|
},
|
||||||
|
[`${tagsEditorRootMobile} &`]: {
|
||||||
|
height: 44,
|
||||||
|
},
|
||||||
|
[`${tagsEditorRootMobile} &[data-focused="true"]`]: {
|
||||||
|
height: 44,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tagEditIcon = style({
|
export const tagEditIcon = style({
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${tagSelectorItem}:hover &`]: {
|
[`:is(${tagSelectorItem}:hover, ${tagsEditorRootMobile}) &`]: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -115,6 +154,18 @@ export const spacer = style({
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const tagSelectorEmpty = style({
|
||||||
|
padding: '10px 8px',
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
height: '34px',
|
||||||
|
selectors: {
|
||||||
|
[`${tagsEditorRootMobile} &`]: {
|
||||||
|
height: 44,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const menuItemListScrollable = style({});
|
export const menuItemListScrollable = style({});
|
||||||
|
|
||||||
export const menuItemListScrollbar = style({
|
export const menuItemListScrollbar = style({
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { globalStyle, style } from '@vanilla-extract/css';
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const menuItemListScrollable = style({});
|
export const menuItemListScrollable = style({});
|
||||||
@@ -30,3 +31,34 @@ export const tagColorIcon = style({
|
|||||||
height: 16,
|
height: 16,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobileTagColorIcon = style({
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: '50%',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mobileTagEditInput = style({
|
||||||
|
height: 'auto',
|
||||||
|
borderRadius: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mobileTagEditDeleteRow = style({
|
||||||
|
color: cssVarV2('button/error'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const spacer = style({
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tagColorIconSelect = style({
|
||||||
|
opacity: 0,
|
||||||
|
color: cssVarV2('button/primary'),
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
selectors: {
|
||||||
|
'&[data-selected="true"]': {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
220
packages/frontend/core/src/components/tags/tag-edit-menu.tsx
Normal file
220
packages/frontend/core/src/components/tags/tag-edit-menu.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import {
|
||||||
|
Input,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
type MenuProps,
|
||||||
|
MenuSeparator,
|
||||||
|
Scrollable,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { DeleteIcon, DoneIcon, TagsIcon } from '@blocksuite/icons/rc';
|
||||||
|
import type { MouseEventHandler, PropsWithChildren } from 'react';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { ConfigModal } from '../mobile';
|
||||||
|
import { TagItem } from './tag';
|
||||||
|
import * as styles from './tag-edit-menu.css';
|
||||||
|
import type { TagColor, TagLike } from './types';
|
||||||
|
|
||||||
|
type TagEditMenuProps = PropsWithChildren<{
|
||||||
|
onTagDelete: (tagId: string) => void;
|
||||||
|
colors: TagColor[];
|
||||||
|
tag: TagLike;
|
||||||
|
onTagChange: (property: keyof TagLike, value: string) => void;
|
||||||
|
jumpToTag?: (tagId: string) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const DesktopTagEditMenu = ({
|
||||||
|
tag,
|
||||||
|
onTagDelete,
|
||||||
|
children,
|
||||||
|
jumpToTag,
|
||||||
|
colors,
|
||||||
|
onTagChange,
|
||||||
|
}: TagEditMenuProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
const menuProps = useMemo(() => {
|
||||||
|
const updateTagName = (name: string) => {
|
||||||
|
if (name.trim() === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onTagChange('value', name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentOptions: {
|
||||||
|
onClick(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
defaultValue={tag.value}
|
||||||
|
onBlur={e => {
|
||||||
|
updateTagName(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
updateTagName(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
placeholder={t['Untitled']()}
|
||||||
|
/>
|
||||||
|
<MenuSeparator />
|
||||||
|
<MenuItem
|
||||||
|
prefixIcon={<DeleteIcon />}
|
||||||
|
type="danger"
|
||||||
|
onClick={() => {
|
||||||
|
tag?.id ? onTagDelete(tag.id) : null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t['Delete']()}
|
||||||
|
</MenuItem>
|
||||||
|
{jumpToTag ? (
|
||||||
|
<MenuItem
|
||||||
|
prefixIcon={<TagsIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
jumpToTag(tag.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t['com.affine.page-properties.tags.open-tags-page']()}
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
|
<MenuSeparator />
|
||||||
|
<Scrollable.Root>
|
||||||
|
<Scrollable.Viewport className={styles.menuItemList}>
|
||||||
|
{colors.map(({ name, value: color }, i) => (
|
||||||
|
<MenuItem
|
||||||
|
key={i}
|
||||||
|
checked={tag.color === color}
|
||||||
|
prefixIcon={
|
||||||
|
<div key={i} className={styles.tagColorIconWrapper}>
|
||||||
|
<div
|
||||||
|
className={styles.tagColorIcon}
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
onTagChange('color', color);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
<Scrollable.Scrollbar className={styles.menuItemListScrollbar} />
|
||||||
|
</Scrollable.Viewport>
|
||||||
|
</Scrollable.Root>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
} satisfies Partial<MenuProps>;
|
||||||
|
}, [tag, t, jumpToTag, colors, onTagChange, onTagDelete]);
|
||||||
|
|
||||||
|
return <Menu {...menuProps}>{children}</Menu>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileTagEditMenu = ({
|
||||||
|
tag,
|
||||||
|
onTagDelete,
|
||||||
|
children,
|
||||||
|
colors,
|
||||||
|
onTagChange,
|
||||||
|
}: TagEditMenuProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const t = useI18n();
|
||||||
|
const [localTag, setLocalTag] = useState({ ...tag });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalTag({ ...tag });
|
||||||
|
}, [tag]);
|
||||||
|
|
||||||
|
const handleTriggerClick: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||||
|
e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpen(true);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const handleOnDone = () => {
|
||||||
|
setOpen(false);
|
||||||
|
if (localTag.value.trim() !== tag.value) {
|
||||||
|
onTagChange('value', localTag.value);
|
||||||
|
}
|
||||||
|
if (localTag.color !== tag.color) {
|
||||||
|
onTagChange('color', localTag.color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConfigModal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={<TagItem mode="list-tag" tag={tag} />}
|
||||||
|
onDone={handleOnDone}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
inputStyle={{
|
||||||
|
height: 46,
|
||||||
|
padding: '12px',
|
||||||
|
}}
|
||||||
|
autoSelect={false}
|
||||||
|
className={styles.mobileTagEditInput}
|
||||||
|
value={localTag.value}
|
||||||
|
onChange={e => {
|
||||||
|
setLocalTag({ ...localTag, value: e });
|
||||||
|
}}
|
||||||
|
placeholder={t['Untitled']()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfigModal.RowGroup title={t['Colors']()}>
|
||||||
|
{colors.map(({ name, value: color }, i) => (
|
||||||
|
<ConfigModal.Row
|
||||||
|
key={i}
|
||||||
|
onClick={() => {
|
||||||
|
setLocalTag({ ...localTag, color });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div key={i} className={styles.tagColorIconWrapper}>
|
||||||
|
<div
|
||||||
|
className={styles.tagColorIcon}
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{name}
|
||||||
|
<div className={styles.spacer} />
|
||||||
|
<DoneIcon
|
||||||
|
className={styles.tagColorIconSelect}
|
||||||
|
data-selected={localTag.color === color}
|
||||||
|
/>
|
||||||
|
</ConfigModal.Row>
|
||||||
|
))}
|
||||||
|
</ConfigModal.RowGroup>
|
||||||
|
|
||||||
|
<ConfigModal.RowGroup>
|
||||||
|
<ConfigModal.Row
|
||||||
|
className={styles.mobileTagEditDeleteRow}
|
||||||
|
onClick={() => {
|
||||||
|
onTagDelete(tag.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
{t['Delete']()}
|
||||||
|
</ConfigModal.Row>
|
||||||
|
</ConfigModal.RowGroup>
|
||||||
|
</ConfigModal>
|
||||||
|
<div onClick={handleTriggerClick}>{children}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TagEditMenu = BUILD_CONFIG.isMobileEdition
|
||||||
|
? MobileTagEditMenu
|
||||||
|
: DesktopTagEditMenu;
|
||||||
@@ -108,7 +108,7 @@ export const tagInlineMode = style([
|
|||||||
export const tagListItemMode = style([
|
export const tagListItemMode = style([
|
||||||
tag,
|
tag,
|
||||||
{
|
{
|
||||||
fontSize: cssVar('fontSm'),
|
fontSize: 'inherit',
|
||||||
padding: '4px 12px',
|
padding: '4px 12px',
|
||||||
columnGap: '8px',
|
columnGap: '8px',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
|
import { IconButton, Menu, RowInput, Scrollable } from '@affine/component';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent, ReactNode } from 'react';
|
||||||
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { IconButton } from '../button';
|
import { ConfigModal } from '../mobile';
|
||||||
import { RowInput } from '../input';
|
|
||||||
import { Menu } from '../menu';
|
|
||||||
import { Scrollable } from '../scrollbar';
|
|
||||||
import { InlineTagList } from './inline-tag-list';
|
import { InlineTagList } from './inline-tag-list';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
import { TagItem } from './tag';
|
import { TagItem } from './tag';
|
||||||
@@ -32,6 +30,7 @@ export interface TagsInlineEditorProps extends TagsEditorProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
title?: ReactNode; // only used for mobile
|
||||||
}
|
}
|
||||||
|
|
||||||
type TagOption = TagLike | { readonly create: true; readonly value: string };
|
type TagOption = TagLike | { readonly create: true; readonly value: string };
|
||||||
@@ -201,7 +200,14 @@ export const TagsEditor = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="tags-editor-popup" className={styles.tagsEditorRoot}>
|
<div
|
||||||
|
data-testid="tags-editor-popup"
|
||||||
|
className={
|
||||||
|
BUILD_CONFIG.isMobileEdition
|
||||||
|
? styles.tagsEditorRootMobile
|
||||||
|
: styles.tagsEditorRoot
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className={styles.tagsEditorSelectedTags}>
|
<div className={styles.tagsEditorSelectedTags}>
|
||||||
<InlineTagList
|
<InlineTagList
|
||||||
tagMode={tagMode}
|
tagMode={tagMode}
|
||||||
@@ -230,6 +236,10 @@ export const TagsEditor = ({
|
|||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className={styles.tagSelectorTagsScrollContainer}
|
className={styles.tagSelectorTagsScrollContainer}
|
||||||
>
|
>
|
||||||
|
{tagOptions.length === 0 && (
|
||||||
|
<div className={styles.tagSelectorEmpty}>Nothing here yet</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{tagOptions.map((tag, idx) => {
|
{tagOptions.map((tag, idx) => {
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
...(safeFocusedIndex === idx ? { focused: 'true' } : {}),
|
...(safeFocusedIndex === idx ? { focused: 'true' } : {}),
|
||||||
@@ -288,7 +298,48 @@ export const TagsEditor = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TagsInlineEditor = ({
|
const MobileInlineEditor = ({
|
||||||
|
readonly,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}: TagsInlineEditorProps) => {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
const empty = !props.selectedTags || props.selectedTags.length === 0;
|
||||||
|
const selectedTags = useMemo(() => {
|
||||||
|
return props.selectedTags
|
||||||
|
.map(id => props.tags.find(tag => tag.id === id))
|
||||||
|
.filter(tag => tag !== undefined);
|
||||||
|
}, [props.selectedTags, props.tags]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConfigModal
|
||||||
|
title={title}
|
||||||
|
open={editing}
|
||||||
|
onOpenChange={setEditing}
|
||||||
|
onBack={() => setEditing(false)}
|
||||||
|
>
|
||||||
|
<TagsEditor {...props} />
|
||||||
|
</ConfigModal>
|
||||||
|
<div
|
||||||
|
className={clsx(styles.tagsInlineEditor, className)}
|
||||||
|
data-empty={empty}
|
||||||
|
data-readonly={readonly}
|
||||||
|
onClick={() => setEditing(true)}
|
||||||
|
>
|
||||||
|
{empty ? (
|
||||||
|
placeholder
|
||||||
|
) : (
|
||||||
|
<InlineTagList {...props} tags={selectedTags} onRemoved={undefined} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DesktopTagsInlineEditor = ({
|
||||||
readonly,
|
readonly,
|
||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
@@ -322,9 +373,18 @@ export const TagsInlineEditor = ({
|
|||||||
{empty ? (
|
{empty ? (
|
||||||
placeholder
|
placeholder
|
||||||
) : (
|
) : (
|
||||||
<InlineTagList {...props} tags={selectedTags} onRemoved={undefined} />
|
<InlineTagList
|
||||||
|
{...props}
|
||||||
|
title=""
|
||||||
|
tags={selectedTags}
|
||||||
|
onRemoved={undefined}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TagsInlineEditor = BUILD_CONFIG.isMobileEdition
|
||||||
|
? MobileInlineEditor
|
||||||
|
: DesktopTagsInlineEditor;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export * from './app-tabs';
|
export * from './app-tabs';
|
||||||
export * from './doc-card';
|
export * from './doc-card';
|
||||||
export * from './page-header';
|
|
||||||
export * from './rename';
|
export * from './rename';
|
||||||
export * from './search-input';
|
export * from './search-input';
|
||||||
export * from './search-result';
|
export * from './search-result';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Scrollable,
|
Scrollable,
|
||||||
useThemeColorMeta,
|
useThemeColorMeta,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
|
import { PageHeader } from '@affine/core/components/mobile';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
@@ -18,7 +19,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { PageHeader } from '../../components/page-header';
|
|
||||||
import * as styles from './generic.css';
|
import * as styles from './generic.css';
|
||||||
|
|
||||||
export interface GenericSelectorProps {
|
export interface GenericSelectorProps {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { footnoteRegular } from '@toeverything/theme/typography';
|
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const group = style({
|
export const group = style({
|
||||||
@@ -8,20 +6,3 @@ export const group = style({
|
|||||||
gap: 4,
|
gap: 4,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const title = style([
|
|
||||||
footnoteRegular,
|
|
||||||
{
|
|
||||||
padding: '0px 8px',
|
|
||||||
color: cssVarV2('text/tertiary'),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const content = style({
|
|
||||||
background: cssVarV2('layer/background/primary'),
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: '10px 16px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 8,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ConfigModal } from '@affine/core/components/mobile';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
@@ -21,15 +22,16 @@ export const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(
|
|||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.group, className)} ref={ref} {...attrs}>
|
<ConfigModal.RowGroup
|
||||||
{title ? <h6 className={styles.title}>{title}</h6> : null}
|
{...attrs}
|
||||||
<div
|
ref={ref}
|
||||||
className={clsx(styles.content, contentClassName)}
|
title={title}
|
||||||
style={contentStyle}
|
className={clsx(styles.group, className)}
|
||||||
>
|
contentClassName={contentClassName}
|
||||||
{children}
|
contentStyle={contentStyle}
|
||||||
</div>
|
>
|
||||||
</div>
|
{children}
|
||||||
|
</ConfigModal.RowGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Modal } from '@affine/component';
|
import { ConfigModal } from '@affine/core/components/mobile';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import type {
|
import type {
|
||||||
DialogComponentProps,
|
DialogComponentProps,
|
||||||
@@ -6,10 +6,8 @@ import type {
|
|||||||
} from '@affine/core/modules/dialogs';
|
} from '@affine/core/modules/dialogs';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { PageHeader } from '../../components';
|
|
||||||
import { AboutGroup } from './about';
|
import { AboutGroup } from './about';
|
||||||
import { AppearanceGroup } from './appearance';
|
import { AppearanceGroup } from './appearance';
|
||||||
import { OthersGroup } from './others';
|
import { OthersGroup } from './others';
|
||||||
@@ -17,50 +15,34 @@ import * as styles from './style.css';
|
|||||||
import { UserProfile } from './user-profile';
|
import { UserProfile } from './user-profile';
|
||||||
import { UserUsage } from './user-usage';
|
import { UserUsage } from './user-usage';
|
||||||
|
|
||||||
const MobileSetting = ({ onClose }: { onClose: () => void }) => {
|
const MobileSetting = () => {
|
||||||
const t = useI18n();
|
|
||||||
const session = useService(AuthService).session;
|
const session = useService(AuthService).session;
|
||||||
|
|
||||||
useEffect(() => session.revalidate(), [session]);
|
useEffect(() => session.revalidate(), [session]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.root}>
|
||||||
<PageHeader back backAction={onClose}>
|
<UserProfile />
|
||||||
<span className={styles.pageTitle}>
|
<UserUsage />
|
||||||
{t['com.affine.mobile.setting.header-title']()}
|
<AppearanceGroup />
|
||||||
</span>
|
<AboutGroup />
|
||||||
</PageHeader>
|
<OthersGroup />
|
||||||
|
</div>
|
||||||
<div className={styles.root}>
|
|
||||||
<UserProfile />
|
|
||||||
<UserUsage />
|
|
||||||
<AppearanceGroup />
|
|
||||||
<AboutGroup />
|
|
||||||
<OthersGroup />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SettingDialog = ({
|
export const SettingDialog = ({
|
||||||
close,
|
close,
|
||||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['setting']>) => {
|
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['setting']>) => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ConfigModal
|
||||||
fullScreen
|
title={t['com.affine.mobile.setting.header-title']()}
|
||||||
animation="slideBottom"
|
|
||||||
open
|
open
|
||||||
onOpenChange={() => close()}
|
onOpenChange={() => close()}
|
||||||
contentOptions={{
|
onBack={close}
|
||||||
style: {
|
|
||||||
padding: 0,
|
|
||||||
overflowY: 'auto',
|
|
||||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
withoutCloseButton
|
|
||||||
>
|
>
|
||||||
<MobileSetting onClose={close} />
|
<MobileSetting />
|
||||||
</Modal>
|
</ConfigModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ConfigModal } from '@affine/core/components/mobile';
|
||||||
import { DualLinkIcon } from '@blocksuite/icons/rc';
|
import { DualLinkIcon } from '@blocksuite/icons/rc';
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
@@ -9,13 +10,13 @@ export const RowLayout = ({
|
|||||||
href,
|
href,
|
||||||
}: PropsWithChildren<{ label: ReactNode; href?: string }>) => {
|
}: PropsWithChildren<{ label: ReactNode; href?: string }>) => {
|
||||||
const content = (
|
const content = (
|
||||||
<div className={styles.baseSettingItem}>
|
<ConfigModal.Row className={styles.baseSettingItem}>
|
||||||
<div className={styles.baseSettingItemName}>{label}</div>
|
<div className={styles.baseSettingItemName}>{label}</div>
|
||||||
<div className={styles.baseSettingItemAction}>
|
<div className={styles.baseSettingItemAction}>
|
||||||
{children ||
|
{children ||
|
||||||
(href ? <DualLinkIcon className={styles.linkIcon} /> : null)}
|
(href ? <DualLinkIcon className={styles.linkIcon} /> : null)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ConfigModal.Row>
|
||||||
);
|
);
|
||||||
|
|
||||||
return href ? (
|
return href ? (
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { style } from '@vanilla-extract/css';
|
|||||||
export const pageTitle = style([bodyEmphasized]);
|
export const pageTitle = style([bodyEmphasized]);
|
||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
padding: '24px 16px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
@@ -16,8 +15,9 @@ export const baseSettingItem = style({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 32,
|
gap: 32,
|
||||||
padding: '8px 0',
|
padding: 8,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const baseSettingItemName = style([
|
export const baseSettingItemName = style([
|
||||||
bodyRegular,
|
bodyRegular,
|
||||||
{
|
{
|
||||||
@@ -26,6 +26,7 @@ export const baseSettingItemName = style([
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const baseSettingItemAction = style([
|
export const baseSettingItemAction = style([
|
||||||
baseSettingItemName,
|
baseSettingItemName,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const UsagePanel = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingGroup title="Storage">
|
<SettingGroup title="Storage" contentStyle={{ padding: '10px 16px' }}>
|
||||||
<CloudUsage />
|
<CloudUsage />
|
||||||
{serverFeatures?.copilot ? <AiUsage /> : null}
|
{serverFeatures?.copilot ? <AiUsage /> : null}
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { bodyRegular, caption1Regular } from '@toeverything/theme/typography';
|
|||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const progressRoot = style({
|
export const progressRoot = style({});
|
||||||
paddingBottom: 8,
|
|
||||||
});
|
|
||||||
export const progressInfoRow = style({
|
export const progressInfoRow = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-
|
|||||||
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
|
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
|
||||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||||
|
import { PageHeader } from '@affine/core/components/mobile';
|
||||||
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||||
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
|
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
@@ -42,7 +43,7 @@ import dayjs from 'dayjs';
|
|||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { AppTabs, PageHeader } from '../../../components';
|
import { AppTabs } from '../../../components';
|
||||||
import { JournalDatePicker } from './journal-date-picker';
|
import { JournalDatePicker } from './journal-date-picker';
|
||||||
import * as styles from './mobile-detail-page.css';
|
import * as styles from './mobile-detail-page.css';
|
||||||
import { PageHeaderMenuButton } from './page-header-more-button';
|
import { PageHeaderMenuButton } from './page-header-more-button';
|
||||||
@@ -215,12 +216,12 @@ const notFound = (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const JournalDetailPage = ({
|
const MobileDetailPage = ({
|
||||||
pageId,
|
pageId,
|
||||||
date,
|
date,
|
||||||
}: {
|
}: {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
date: string;
|
date?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const journalService = useService(JournalService);
|
const journalService = useService(JournalService);
|
||||||
const { openJournal } = useJournalRouteHelper();
|
const { openJournal } = useJournalRouteHelper();
|
||||||
@@ -250,40 +251,23 @@ const JournalDetailPage = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className={bodyEmphasized}>
|
{date ? (
|
||||||
{i18nTime(dayjs(date), { absolute: { accuracy: 'month' } })}
|
<span className={bodyEmphasized}>
|
||||||
</span>
|
{i18nTime(dayjs(date), { absolute: { accuracy: 'month' } })}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<JournalDatePicker
|
{date ? (
|
||||||
date={date}
|
<JournalDatePicker
|
||||||
onChange={handleDateChange}
|
date={date}
|
||||||
withDotDates={allJournalDates}
|
onChange={handleDateChange}
|
||||||
/>
|
withDotDates={allJournalDates}
|
||||||
<DetailPageImpl />
|
/>
|
||||||
<AppTabs background={cssVarV2('layer/background/primary')} />
|
) : null}
|
||||||
</DetailPageWrapper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const NormalDetailPage = ({ pageId }: { pageId: string }) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.root}>
|
|
||||||
<DetailPageWrapper
|
|
||||||
skeleton={skeleton}
|
|
||||||
notFound={notFound}
|
|
||||||
pageId={pageId}
|
|
||||||
>
|
|
||||||
<PageHeader
|
|
||||||
back
|
|
||||||
className={styles.header}
|
|
||||||
suffix={
|
|
||||||
<>
|
|
||||||
<PageHeaderShareButton />
|
|
||||||
<PageHeaderMenuButton />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<DetailPageImpl />
|
<DetailPageImpl />
|
||||||
|
{date ? (
|
||||||
|
<AppTabs background={cssVarV2('layer/background/primary')} />
|
||||||
|
) : null}
|
||||||
</DetailPageWrapper>
|
</DetailPageWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -300,9 +284,5 @@ export const Component = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return journalDate ? (
|
return <MobileDetailPage pageId={pageId} date={journalDate} />;
|
||||||
<JournalDetailPage pageId={pageId} date={journalDate} />
|
|
||||||
) : (
|
|
||||||
<NormalDetailPage pageId={pageId} />
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IconButton, notify } from '@affine/component';
|
import { IconButton, notify } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
MenuSeparator,
|
MenuSeparator,
|
||||||
|
MenuSub,
|
||||||
MobileMenu,
|
MobileMenu,
|
||||||
MobileMenuItem,
|
MobileMenuItem,
|
||||||
} from '@affine/component/ui/menu';
|
} from '@affine/component/ui/menu';
|
||||||
@@ -42,6 +43,7 @@ export const PageHeaderMenuButton = () => {
|
|||||||
editorService.editor.doc.meta$.map(meta => meta.trash)
|
editorService.editor.doc.meta$.map(meta => meta.trash)
|
||||||
);
|
);
|
||||||
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
|
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
|
||||||
|
const title = useLiveData(editorService.editor.doc.title$);
|
||||||
|
|
||||||
const { favorite, toggleFavorite } = useFavorite(docId);
|
const { favorite, toggleFavorite } = useFavorite(docId);
|
||||||
|
|
||||||
@@ -104,14 +106,16 @@ export const PageHeaderMenuButton = () => {
|
|||||||
: t['com.affine.favoritePageOperation.add']()}
|
: t['com.affine.favoritePageOperation.add']()}
|
||||||
</MobileMenuItem>
|
</MobileMenuItem>
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
<MobileMenu items={<DocInfoSheet docId={docId} />}>
|
<MenuSub
|
||||||
<MobileMenuItem
|
triggerOptions={{
|
||||||
prefixIcon={<InformationIcon />}
|
prefixIcon: <InformationIcon />,
|
||||||
onClick={preventDefault}
|
onClick: preventDefault,
|
||||||
>
|
}}
|
||||||
<span>{t['com.affine.page-properties.page-info.view']()}</span>
|
title={title ?? t['unnamed']()}
|
||||||
</MobileMenuItem>
|
items={<DocInfoSheet docId={docId} />}
|
||||||
</MobileMenu>
|
>
|
||||||
|
<span>{t['com.affine.page-properties.page-info.view']()}</span>
|
||||||
|
</MenuSub>
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
items={
|
items={
|
||||||
<div className={styles.outlinePanel}>
|
<div className={styles.outlinePanel}>
|
||||||
|
|||||||
@@ -1,13 +1,49 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const linksRow = style({
|
export const scrollableRoot = style({
|
||||||
padding: '0 16px',
|
padding: '0 16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const timeRow = style({
|
export const linksRow = style({});
|
||||||
padding: '0 16px',
|
|
||||||
});
|
export const timeRow = style({});
|
||||||
export const scrollBar = style({
|
export const scrollBar = style({
|
||||||
width: 6,
|
width: 6,
|
||||||
transform: 'translateX(-4px)',
|
transform: 'translateX(-4px)',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const tableBodyRoot = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
});
|
||||||
|
export const addPropertyButton = style({
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
color: `${cssVarV2('text/secondary')}`,
|
||||||
|
padding: '0 4px',
|
||||||
|
height: 36,
|
||||||
|
fontWeight: 400,
|
||||||
|
gap: 6,
|
||||||
|
'@media': {
|
||||||
|
print: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
[`[data-property-collapsed="true"] &`]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
globalStyle(`${addPropertyButton} svg`, {
|
||||||
|
fontSize: 16,
|
||||||
|
color: cssVarV2('icon/secondary'),
|
||||||
|
});
|
||||||
|
globalStyle(`${addPropertyButton}:hover svg`, {
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,25 +1,43 @@
|
|||||||
import { Divider, Scrollable } from '@affine/component';
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Menu,
|
||||||
|
PropertyCollapsibleContent,
|
||||||
|
PropertyCollapsibleSection,
|
||||||
|
Scrollable,
|
||||||
|
} from '@affine/component';
|
||||||
import {
|
import {
|
||||||
type DefaultOpenProperty,
|
type DefaultOpenProperty,
|
||||||
DocPropertiesTable,
|
DocPropertyRow,
|
||||||
} from '@affine/core/components/doc-properties';
|
} from '@affine/core/components/doc-properties';
|
||||||
|
import { CreatePropertyMenuItems } from '@affine/core/components/doc-properties/menu/create-doc-property';
|
||||||
import { LinksRow } from '@affine/core/desktop/dialogs/doc-info/links-row';
|
import { LinksRow } from '@affine/core/desktop/dialogs/doc-info/links-row';
|
||||||
import { TimeRow } from '@affine/core/desktop/dialogs/doc-info/time-row';
|
import { TimeRow } from '@affine/core/desktop/dialogs/doc-info/time-row';
|
||||||
|
import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
|
||||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import { Suspense, useMemo } from 'react';
|
import {
|
||||||
|
type DocCustomPropertyInfo,
|
||||||
|
DocsService,
|
||||||
|
LiveData,
|
||||||
|
useLiveData,
|
||||||
|
useServices,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './doc-info.css';
|
import * as styles from './doc-info.css';
|
||||||
|
|
||||||
export const DocInfoSheet = ({
|
export const DocInfoSheet = ({
|
||||||
docId,
|
docId,
|
||||||
defaultOpenProperty,
|
|
||||||
}: {
|
}: {
|
||||||
docId: string;
|
docId: string;
|
||||||
defaultOpenProperty?: DefaultOpenProperty;
|
defaultOpenProperty?: DefaultOpenProperty;
|
||||||
}) => {
|
}) => {
|
||||||
const docsSearchService = useService(DocsSearchService);
|
const { docsSearchService, docsService } = useServices({
|
||||||
|
DocsSearchService,
|
||||||
|
DocsService,
|
||||||
|
});
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const links = useLiveData(
|
const links = useLiveData(
|
||||||
@@ -35,8 +53,16 @@ export const DocInfoSheet = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const onPropertyAdded = useCallback((property: DocCustomPropertyInfo) => {
|
||||||
|
setNewPropertyId(property.id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const properties = useLiveData(docsService.propertyList.sortedProperties$);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Scrollable.Root>
|
<Scrollable.Root className={styles.scrollableRoot}>
|
||||||
<Scrollable.Viewport data-testid="doc-info-menu">
|
<Scrollable.Viewport data-testid="doc-info-menu">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<TimeRow docId={docId} className={styles.timeRow} />
|
<TimeRow docId={docId} className={styles.timeRow} />
|
||||||
@@ -61,7 +87,58 @@ export const DocInfoSheet = ({
|
|||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<DocPropertiesTable defaultOpenProperty={defaultOpenProperty} />
|
|
||||||
|
<PropertyCollapsibleSection
|
||||||
|
title={t.t('com.affine.workspace.properties')}
|
||||||
|
>
|
||||||
|
<PropertyCollapsibleContent
|
||||||
|
className={styles.tableBodyRoot}
|
||||||
|
collapseButtonText={({ hide, isCollapsed }) =>
|
||||||
|
isCollapsed
|
||||||
|
? hide === 1
|
||||||
|
? t['com.affine.page-properties.more-property.one']({
|
||||||
|
count: hide.toString(),
|
||||||
|
})
|
||||||
|
: t['com.affine.page-properties.more-property.more']({
|
||||||
|
count: hide.toString(),
|
||||||
|
})
|
||||||
|
: hide === 1
|
||||||
|
? t['com.affine.page-properties.hide-property.one']({
|
||||||
|
count: hide.toString(),
|
||||||
|
})
|
||||||
|
: t['com.affine.page-properties.hide-property.more']({
|
||||||
|
count: hide.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{properties.map(property => (
|
||||||
|
<DocPropertyRow
|
||||||
|
key={property.id}
|
||||||
|
propertyInfo={property}
|
||||||
|
defaultOpenEditMenu={newPropertyId === property.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Menu
|
||||||
|
items={<CreatePropertyMenuItems onCreated={onPropertyAdded} />}
|
||||||
|
contentOptions={{
|
||||||
|
onClick(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
prefix={<PlusIcon />}
|
||||||
|
className={styles.addPropertyButton}
|
||||||
|
>
|
||||||
|
{t['com.affine.page-properties.add-property']()}
|
||||||
|
</Button>
|
||||||
|
</Menu>
|
||||||
|
</PropertyCollapsibleContent>
|
||||||
|
</PropertyCollapsibleSection>
|
||||||
|
<Divider size="thinner" />
|
||||||
|
|
||||||
|
<DocDatabaseBacklinkInfo />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Scrollable.Viewport>
|
</Scrollable.Viewport>
|
||||||
<Scrollable.Scrollbar className={styles.scrollBar} />
|
<Scrollable.Scrollbar className={styles.scrollBar} />
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { IconButton, MobileMenu } from '@affine/component';
|
import { IconButton, MobileMenu } from '@affine/component';
|
||||||
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
|
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
|
||||||
|
import { PageHeader } from '@affine/core/components/mobile';
|
||||||
import { isEmptyCollection } from '@affine/core/desktop/pages/workspace/collection';
|
import { isEmptyCollection } from '@affine/core/desktop/pages/workspace/collection';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
import { PageHeader } from '../../../components';
|
|
||||||
import { AllDocList } from '../doc/list';
|
import { AllDocList } from '../doc/list';
|
||||||
import { AllDocsMenu } from '../doc/menu';
|
import { AllDocsMenu } from '../doc/menu';
|
||||||
import * as styles from './detail.css';
|
import * as styles from './detail.css';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IconButton, MobileMenu } from '@affine/component';
|
import { IconButton, MobileMenu } from '@affine/component';
|
||||||
|
import { PageHeader } from '@affine/core/components/mobile';
|
||||||
import type { Tag } from '@affine/core/modules/tag';
|
import type { Tag } from '@affine/core/modules/tag';
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData } from '@toeverything/infra';
|
import { useLiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
import { PageHeader } from '../../../components';
|
|
||||||
import { AllDocsMenu } from '../doc';
|
import { AllDocsMenu } from '../doc';
|
||||||
import * as styles from './detail.css';
|
import * as styles from './detail.css';
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ export const textarea = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mobileTextareaBase = {
|
||||||
|
fontSize: 17,
|
||||||
|
lineHeight: '26px',
|
||||||
|
padding: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mobileTextareaPlain = style([
|
||||||
|
textarea,
|
||||||
|
mobileTextareaBase,
|
||||||
|
{
|
||||||
|
position: 'relative',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '22px',
|
||||||
|
height: 'auto',
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const mobileTextarea = style([textarea, mobileTextareaBase]);
|
||||||
|
|
||||||
export const container = style({
|
export const container = style({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
outline: `1px solid transparent`,
|
outline: `1px solid transparent`,
|
||||||
@@ -54,3 +74,11 @@ export const textInvisible = style({
|
|||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobileTextInvisible = style([textInvisible, mobileTextareaBase]);
|
||||||
|
|
||||||
|
export const mobileTextareaWrapper = style({
|
||||||
|
position: 'relative',
|
||||||
|
background: cssVarV2('layer/background/primary'),
|
||||||
|
borderRadius: 12,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
import { AffinePageReference } from '@affine/core/components/affine/reference-link';
|
import { AffinePageReference } from '@affine/core/components/affine/reference-link';
|
||||||
|
import { ConfigModal } from '@affine/core/components/mobile';
|
||||||
import { resolveLinkToDoc } from '@affine/core/modules/navigation';
|
import { resolveLinkToDoc } from '@affine/core/modules/navigation';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { LinkIcon } from '@blocksuite/icons/rc';
|
||||||
import type { LiveData } from '@toeverything/infra';
|
import type { LiveData } from '@toeverything/infra';
|
||||||
import { useLiveData } from '@toeverything/infra';
|
import { useLiveData } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
@@ -99,49 +101,84 @@ export const LinkCell = ({
|
|||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
|
const editingElement = (
|
||||||
|
<>
|
||||||
|
<textarea
|
||||||
|
ref={ref}
|
||||||
|
onKeyDown={onKeydown}
|
||||||
|
className={
|
||||||
|
!BUILD_CONFIG.isMobileEdition
|
||||||
|
? styles.textarea
|
||||||
|
: styles.mobileTextarea
|
||||||
|
}
|
||||||
|
onBlur={commitChange}
|
||||||
|
value={tempValue || ''}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
data-empty={!tempValue}
|
||||||
|
placeholder={t[
|
||||||
|
'com.affine.page-properties.property-value-placeholder'
|
||||||
|
]()}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
!BUILD_CONFIG.isMobileEdition
|
||||||
|
? styles.textInvisible
|
||||||
|
: styles.mobileTextInvisible
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tempValue}
|
||||||
|
{tempValue?.endsWith('\n') || !tempValue ? <br /> : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const name = useLiveData(cell.property.name$);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyValue
|
<>
|
||||||
className={styles.container}
|
<PropertyValue
|
||||||
isEmpty={isEmpty}
|
className={styles.container}
|
||||||
onClick={onClick}
|
isEmpty={isEmpty}
|
||||||
>
|
onClick={onClick}
|
||||||
{!editing ? (
|
>
|
||||||
resolvedDocLink ? (
|
{!editing ? (
|
||||||
<AffinePageReference
|
resolvedDocLink ? (
|
||||||
pageId={resolvedDocLink.docId}
|
<AffinePageReference
|
||||||
params={resolvedDocLink.params}
|
pageId={resolvedDocLink.docId}
|
||||||
/>
|
params={resolvedDocLink.params}
|
||||||
) : (
|
/>
|
||||||
<a
|
) : (
|
||||||
href={link}
|
<a
|
||||||
target="_blank"
|
href={link}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
onClick={onLinkClick}
|
rel="noopener noreferrer"
|
||||||
className={styles.link}
|
onClick={onLinkClick}
|
||||||
>
|
className={styles.link}
|
||||||
{link?.replace(/^https?:\/\//, '').trim()}
|
>
|
||||||
</a>
|
{link?.replace(/^https?:\/\//, '').trim()}
|
||||||
)
|
</a>
|
||||||
) : (
|
)
|
||||||
<>
|
) : !BUILD_CONFIG.isMobileEdition ? (
|
||||||
<textarea
|
editingElement
|
||||||
ref={ref}
|
) : null}
|
||||||
onKeyDown={onKeydown}
|
</PropertyValue>
|
||||||
className={styles.textarea}
|
{BUILD_CONFIG.isMobileEdition ? (
|
||||||
onBlur={commitChange}
|
<ConfigModal
|
||||||
value={tempValue || ''}
|
open={editing}
|
||||||
onChange={handleOnChange}
|
onOpenChange={setEditing}
|
||||||
data-empty={!tempValue}
|
onBack={() => {
|
||||||
placeholder={t[
|
setEditing(false);
|
||||||
'com.affine.page-properties.property-value-placeholder'
|
}}
|
||||||
]()}
|
title={
|
||||||
/>
|
<>
|
||||||
<div className={styles.textInvisible}>
|
<LinkIcon />
|
||||||
{tempValue}
|
{name}
|
||||||
{tempValue?.endsWith('\n') || !tempValue ? <br /> : null}
|
</>
|
||||||
</div>
|
}
|
||||||
</>
|
>
|
||||||
)}
|
<div className={styles.mobileTextareaWrapper}>{editingElement}</div>
|
||||||
</PropertyValue>
|
</ConfigModal>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Progress, PropertyValue } from '@affine/component';
|
import { Progress, PropertyValue } from '@affine/component';
|
||||||
|
import { ConfigModal } from '@affine/core/components/mobile';
|
||||||
|
import { ProgressIcon } from '@blocksuite/icons/rc';
|
||||||
import type { LiveData } from '@toeverything/infra';
|
import type { LiveData } from '@toeverything/infra';
|
||||||
import { useLiveData } from '@toeverything/infra';
|
import { useLiveData } from '@toeverything/infra';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import type { DatabaseCellRendererProps } from '../../../types';
|
import type { DatabaseCellRendererProps } from '../../../types';
|
||||||
|
|
||||||
export const ProgressCell = ({
|
const DesktopProgressCell = ({
|
||||||
cell,
|
cell,
|
||||||
dataSource,
|
dataSource,
|
||||||
rowId,
|
rowId,
|
||||||
@@ -34,3 +36,62 @@ export const ProgressCell = ({
|
|||||||
</PropertyValue>
|
</PropertyValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MobileProgressCell = ({
|
||||||
|
cell,
|
||||||
|
dataSource,
|
||||||
|
rowId,
|
||||||
|
onChange,
|
||||||
|
}: DatabaseCellRendererProps) => {
|
||||||
|
const value = useLiveData(cell.value$ as LiveData<number>);
|
||||||
|
const isEmpty = value === undefined;
|
||||||
|
const [localValue, setLocalValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const name = useLiveData(cell.property.name$);
|
||||||
|
|
||||||
|
const commitChange = () => {
|
||||||
|
dataSource.cellValueChange(rowId, cell.id, localValue);
|
||||||
|
onChange?.(localValue);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PropertyValue
|
||||||
|
isEmpty={isEmpty}
|
||||||
|
hoverable={false}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Progress value={value} />
|
||||||
|
</PropertyValue>
|
||||||
|
|
||||||
|
<ConfigModal
|
||||||
|
variant="popup"
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onDone={commitChange}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<ProgressIcon />
|
||||||
|
{name}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Progress
|
||||||
|
value={localValue}
|
||||||
|
onChange={v => {
|
||||||
|
setLocalValue(v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ConfigModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const ProgressCell = BUILD_CONFIG.isMobileEdition
|
||||||
|
? MobileProgressCell
|
||||||
|
: DesktopProgressCell;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
|
import { ConfigModal } from '@affine/core/components/mobile';
|
||||||
import type { BlockStdScope } from '@blocksuite/affine/block-std';
|
import type { BlockStdScope } from '@blocksuite/affine/block-std';
|
||||||
import {
|
import {
|
||||||
DefaultInlineManagerExtension,
|
DefaultInlineManagerExtension,
|
||||||
RichText,
|
RichText,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import type { Doc } from '@blocksuite/affine/store';
|
import type { Doc } from '@blocksuite/affine/store';
|
||||||
|
import { TextIcon } from '@blocksuite/icons/rc';
|
||||||
import { type LiveData, useLiveData } from '@toeverything/infra';
|
import { type LiveData, useLiveData } from '@toeverything/infra';
|
||||||
import { useEffect, useRef } from 'react';
|
import { type CSSProperties, useEffect, useRef, useState } from 'react';
|
||||||
import type * as Y from 'yjs';
|
import type * as Y from 'yjs';
|
||||||
|
|
||||||
import type { DatabaseCellRendererProps } from '../../../types';
|
import type { DatabaseCellRendererProps } from '../../../types';
|
||||||
@@ -37,11 +39,12 @@ const renderRichText = ({
|
|||||||
return richText;
|
return richText;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RichTextCell = ({
|
const RichTextInput = ({
|
||||||
cell,
|
cell,
|
||||||
dataSource,
|
dataSource,
|
||||||
onChange,
|
onChange,
|
||||||
}: DatabaseCellRendererProps) => {
|
style,
|
||||||
|
}: DatabaseCellRendererProps & { style?: CSSProperties }) => {
|
||||||
const std = useBlockStdScope(dataSource.doc);
|
const std = useBlockStdScope(dataSource.doc);
|
||||||
const text = useLiveData(cell.value$ as LiveData<Y.Text>);
|
const text = useLiveData(cell.value$ as LiveData<Y.Text>);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@@ -64,5 +67,68 @@ export const RichTextCell = ({
|
|||||||
}
|
}
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [dataSource.doc, onChange, std, text]);
|
}, [dataSource.doc, onChange, std, text]);
|
||||||
return <PropertyValue ref={ref}></PropertyValue>;
|
return <div ref={ref} style={style} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DesktopRichTextCell = ({
|
||||||
|
cell,
|
||||||
|
dataSource,
|
||||||
|
onChange,
|
||||||
|
rowId,
|
||||||
|
}: DatabaseCellRendererProps) => {
|
||||||
|
return (
|
||||||
|
<PropertyValue>
|
||||||
|
<RichTextInput
|
||||||
|
cell={cell}
|
||||||
|
dataSource={dataSource}
|
||||||
|
onChange={onChange}
|
||||||
|
rowId={rowId}
|
||||||
|
/>
|
||||||
|
</PropertyValue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileRichTextCell = ({
|
||||||
|
cell,
|
||||||
|
dataSource,
|
||||||
|
onChange,
|
||||||
|
rowId,
|
||||||
|
}: DatabaseCellRendererProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const name = useLiveData(cell.property.name$);
|
||||||
|
return (
|
||||||
|
<PropertyValue onClick={() => setOpen(true)}>
|
||||||
|
<ConfigModal
|
||||||
|
onBack={() => setOpen(false)}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<TextIcon />
|
||||||
|
{name}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConfigModal.RowGroup>
|
||||||
|
<RichTextInput
|
||||||
|
cell={cell}
|
||||||
|
dataSource={dataSource}
|
||||||
|
onChange={onChange}
|
||||||
|
rowId={rowId}
|
||||||
|
style={{ padding: 12 }}
|
||||||
|
/>
|
||||||
|
</ConfigModal.RowGroup>
|
||||||
|
</ConfigModal>
|
||||||
|
<RichTextInput
|
||||||
|
cell={cell}
|
||||||
|
dataSource={dataSource}
|
||||||
|
onChange={onChange}
|
||||||
|
rowId={rowId}
|
||||||
|
/>
|
||||||
|
</PropertyValue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RichTextCell = BUILD_CONFIG.isMobileEdition
|
||||||
|
? MobileRichTextCell
|
||||||
|
: DesktopRichTextCell;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable rxjs/finnish */
|
/* eslint-disable rxjs/finnish */
|
||||||
|
|
||||||
import { PropertyValue } from '@affine/component';
|
import { PropertyValue } from '@affine/component';
|
||||||
import { type TagLike, TagsInlineEditor } from '@affine/component/ui/tags';
|
import { type TagLike, TagsInlineEditor } from '@affine/core/components/tags';
|
||||||
import { TagService } from '@affine/core/modules/tag';
|
import { TagService } from '@affine/core/modules/tag';
|
||||||
import {
|
import {
|
||||||
affineLabelToDatabaseTagColor,
|
affineLabelToDatabaseTagColor,
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@affine/core/modules/tag/entities/utils';
|
} from '@affine/core/modules/tag/entities/utils';
|
||||||
import type { DatabaseBlockDataSource } from '@blocksuite/affine/blocks';
|
import type { DatabaseBlockDataSource } from '@blocksuite/affine/blocks';
|
||||||
import type { SelectTag } from '@blocksuite/data-view';
|
import type { SelectTag } from '@blocksuite/data-view';
|
||||||
|
import { MultiSelectIcon, SingleSelectIcon } from '@blocksuite/icons/rc';
|
||||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
@@ -221,6 +222,8 @@ const BlocksuiteDatabaseSelector = ({
|
|||||||
[dataSource, selectCell]
|
[dataSource, selectCell]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const propertyName = useLiveData(cell.property.name$);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TagsInlineEditor
|
<TagsInlineEditor
|
||||||
tagMode="db-label"
|
tagMode="db-label"
|
||||||
@@ -233,6 +236,12 @@ const BlocksuiteDatabaseSelector = ({
|
|||||||
onSelectTag={onSelectTag}
|
onSelectTag={onSelectTag}
|
||||||
tagColors={tagColors}
|
tagColors={tagColors}
|
||||||
onTagChange={onTagChange}
|
onTagChange={onTagChange}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{multiple ? <MultiSelectIcon /> : <SingleSelectIcon />}
|
||||||
|
{propertyName}
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,9 +34,31 @@ export const docRefLink = style({
|
|||||||
color: cssVarV2('text/tertiary'),
|
color: cssVarV2('text/tertiary'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mobileDocRefLink = style([
|
||||||
|
docRefLink,
|
||||||
|
{
|
||||||
|
maxWidth: '110px',
|
||||||
|
minWidth: '60px',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const cellList = style({
|
export const cellList = style({
|
||||||
padding: '0 2px',
|
padding: '0 2px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const databaseNameWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const databaseName = style({
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'inline-block',
|
||||||
|
});
|
||||||
|
|||||||
@@ -122,12 +122,24 @@ const DatabaseBacklinkRow = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PropertyCollapsibleSection
|
<PropertyCollapsibleSection
|
||||||
title={(row.databaseName || t['unnamed']()) + ' ' + t['properties']()}
|
// @ts-expect-error fix type
|
||||||
|
title={
|
||||||
|
<span className={styles.databaseNameWrapper}>
|
||||||
|
<span className={styles.databaseName}>
|
||||||
|
{row.databaseName || t['unnamed']()}
|
||||||
|
</span>
|
||||||
|
{t['properties']()}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
defaultCollapsed={!defaultOpen}
|
defaultCollapsed={!defaultOpen}
|
||||||
icon={<DatabaseTableViewIcon />}
|
icon={<DatabaseTableViewIcon />}
|
||||||
suffix={
|
suffix={
|
||||||
<AffinePageReference
|
<AffinePageReference
|
||||||
className={styles.docRefLink}
|
className={
|
||||||
|
BUILD_CONFIG.isMobileEdition
|
||||||
|
? styles.mobileDocRefLink
|
||||||
|
: styles.docRefLink
|
||||||
|
}
|
||||||
pageId={row.docId}
|
pageId={row.docId}
|
||||||
params={pageRefParams}
|
params={pageRefParams}
|
||||||
Icon={PageIcon}
|
Icon={PageIcon}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
"Created": "Created",
|
"Created": "Created",
|
||||||
"Customize": "Customise",
|
"Customize": "Customise",
|
||||||
|
"Colors": "Colors",
|
||||||
"DB_FILE_ALREADY_LOADED": "Database file already loaded",
|
"DB_FILE_ALREADY_LOADED": "Database file already loaded",
|
||||||
"DB_FILE_INVALID": "Invalid database file",
|
"DB_FILE_INVALID": "Invalid database file",
|
||||||
"DB_FILE_MIGRATION_FAILED": "Database file migration failed",
|
"DB_FILE_MIGRATION_FAILED": "Database file migration failed",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ test('doc info', async ({ page }) => {
|
|||||||
await expect(page.getByRole('dialog')).toBeVisible();
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'view info' }).click();
|
await page.getByRole('menuitem', { name: 'view info' }).click();
|
||||||
await expect(page.getByRole('button', { name: 'Back' })).toBeVisible();
|
await expect(page.getByTestId('mobile-menu-back-button')).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByRole('dialog')).toContainText('Created');
|
await expect(page.getByRole('dialog')).toContainText('Created');
|
||||||
await expect(page.getByRole('dialog')).toContainText('Updated');
|
await expect(page.getByRole('dialog')).toContainText('Updated');
|
||||||
|
|||||||
Reference in New Issue
Block a user