feat: support modified cell in modal (#3948)

* feat: support drag fields in modal

* fix: wrong draggable position
This commit is contained in:
Kilu.He 2023-11-17 10:56:25 +08:00 committed by GitHub
parent 729b8571b5
commit 68de83c611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1414 additions and 301 deletions

View File

@ -1,4 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="6" r="1" fill="#333333"/>
<circle cx="8" cy="10" r="1" fill="#333333"/>
<path d="M5 8C5 8.55228 4.55228 9 4 9C3.44772 9 3 8.55228 3 8C3 7.44772 3.44772 7 4 7C4.55228 7 5 7.44772 5 8Z" fill="#78797D"/>
<path d="M9 8C9 8.55228 8.55229 9 8 9C7.44772 9 7 8.55228 7 8C7 7.44772 7.44772 7 8 7C8.55229 7 9 7.44772 9 8Z" fill="#78797D"/>
<path d="M12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9Z" fill="#78797D"/>
</svg>

Before

Width:  |  Height:  |  Size: 194 B

After

Width:  |  Height:  |  Size: 499 B

View File

@ -0,0 +1,9 @@
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fill-rule='evenodd'
clip-rule='evenodd'
d='M7.68372 17.3771C8.88359 18.1747 10.3278 18.75 12.0001 18.75C15.7261 18.75 18.32 15.8941 19.6011 14.0863C20.4933 12.8272 20.4933 11.1728 19.6011 9.91375C19.1009 9.208 18.4007 8.34252 17.5112 7.54957L16.4489 8.61191C17.236 9.30133 17.8836 10.0844 18.3772 10.781C18.9013 11.5205 18.9013 12.4795 18.3772 13.219C17.1411 14.9633 14.9396 17.25 12.0001 17.25C10.794 17.25 9.71218 16.865 8.77028 16.2905L7.68372 17.3771ZM7.55137 15.3881L6.48903 16.4504C5.5995 15.6575 4.8993 14.792 4.39916 14.0863C3.50692 12.8272 3.50692 11.1728 4.39916 9.91375C5.68028 8.10595 8.27417 5.25 12.0001 5.25C13.6724 5.25 15.1167 5.82531 16.3165 6.62294L15.23 7.7095C14.2881 7.13497 13.2062 6.75 12.0001 6.75C9.06064 6.75 6.85914 9.03672 5.62301 10.781C5.09897 11.5205 5.09897 12.4795 5.62301 13.219C6.11667 13.9156 6.76428 14.6987 7.55137 15.3881ZM10.4887 14.572C10.9279 14.8431 11.4439 15 12.0002 15C13.641 15 14.932 13.6349 14.932 12C14.932 11.4625 14.7925 10.9542 14.5468 10.5139L13.3964 11.6644C13.4197 11.7717 13.432 11.884 13.432 12C13.432 12.8503 12.7694 13.5 12.0002 13.5C11.868 13.5 11.739 13.4808 11.616 13.4448L10.4887 14.572ZM10.6039 12.3355L9.45347 13.486C9.20788 13.0458 9.06836 12.5375 9.06836 12C9.06836 10.3651 10.3594 9 12.0002 9C12.5564 9 13.0724 9.15686 13.5115 9.42792L12.3842 10.5552C12.2612 10.5192 12.1323 10.5 12.0002 10.5C11.231 10.5 10.5684 11.1497 10.5684 12C10.5684 12.116 10.5807 12.2282 10.6039 12.3355Z'
fill='currentColor'
/>
<path d='M17.5 5L5 17.5' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,16 @@
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M5.01097 13.6526C4.30282 12.6533 4.30282 11.3467 5.01097 10.3474C6.26959 8.57133 8.66728 6 12 6C15.3327 6 17.7304 8.57133 18.989 10.3474C19.6972 11.3467 19.6972 12.6533 18.989 13.6526C17.7304 15.4287 15.3327 18 12 18C8.66728 18 6.26959 15.4287 5.01097 13.6526Z'
stroke='currentColor'
stroke-width='1.5'
stroke-linecap='round'
stroke-linejoin='round'
/>
<path
d='M11.9999 14.25C13.2049 14.25 14.1818 13.2426 14.1818 12C14.1818 10.7574 13.2049 9.75 11.9999 9.75C10.7949 9.75 9.81812 10.7574 9.81812 12C9.81812 13.2426 10.7949 14.25 11.9999 14.25Z'
stroke='currentColor'
stroke-width='1.5'
stroke-linecap='round'
stroke-linejoin='round'
/>
</svg>

After

Width:  |  Height:  |  Size: 888 B

View File

@ -0,0 +1,6 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 11H1V8" stroke="#828282" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 1H11V4" stroke="#828282" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 11L5 7" stroke="#828282" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 1L7 5" stroke="#828282" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@ -114,7 +114,7 @@ function BlockDragDropContext({ children }: { children: React.ReactNode }) {
left: draggingPosition?.x,
pointerEvents: 'none',
opacity: dragShadowVisible ? 1 : 0,
zIndex: 1000,
zIndex: 2000,
}}
/>
</>

View File

@ -0,0 +1,34 @@
import ViewIconGroup from '$app/components/_shared/ViewTitle/ViewIconGroup';
import { PageIcon } from '$app_reducers/pages/slice';
import ViewIcon from '$app/components/_shared/ViewTitle/ViewIcon';
function ViewBanner({
icon,
hover,
onUpdateIcon,
}: {
icon?: PageIcon;
hover: boolean;
onUpdateIcon: (icon: string) => void;
}) {
return (
<>
<div
style={{
display: icon ? 'flex' : 'none',
}}
>
<ViewIcon onUpdateIcon={onUpdateIcon} icon={icon} />
</div>
<div
style={{
opacity: hover ? 1 : 0,
}}
>
<ViewIconGroup icon={icon} onUpdateIcon={onUpdateIcon} />
</div>
</>
);
}
export default ViewBanner;

View File

@ -0,0 +1,54 @@
import React, { useCallback, useState } from 'react';
import Popover from '@mui/material/Popover';
import EmojiPicker from '$app/components/_shared/EmojiPicker';
import { PageIcon } from '$app_reducers/pages/slice';
function ViewIcon({ icon, onUpdateIcon }: { icon?: PageIcon; onUpdateIcon: (icon: string) => void }) {
const [anchorPosition, setAnchorPosition] = useState<{
top: number;
left: number;
}>();
const open = Boolean(anchorPosition);
const onOpen = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
setAnchorPosition({
top: rect.top + rect.height,
left: rect.left,
});
}, []);
const onEmojiSelect = useCallback(
(emoji: string) => {
onUpdateIcon(emoji);
setAnchorPosition(undefined);
},
[onUpdateIcon]
);
if (!icon) return null;
return (
<>
<div className={`-ml-2 flex rounded p-2 hover:bg-content-blue-50`}>
<div onClick={onOpen} className={'h-full w-full cursor-pointer rounded text-6xl'}>
{icon.value}
</div>
</div>
{open && (
<Popover
open={open}
anchorReference='anchorPosition'
anchorPosition={anchorPosition}
disableAutoFocus
disableRestoreFocus
onClose={() => setAnchorPosition(undefined)}
>
<EmojiPicker onEmojiSelect={onEmojiSelect} />
</Popover>
)}
</>
);
}
export default ViewIcon;

View File

@ -0,0 +1,46 @@
import { useTranslation } from 'react-i18next';
import { PageIcon } from '$app_reducers/pages/slice';
import React, { useCallback } from 'react';
import { randomEmoji } from '$app/utils/document/emoji';
import { EmojiEmotionsOutlined } from '@mui/icons-material';
import Button from '@mui/material/Button';
interface Props {
icon?: PageIcon;
// onUpdateCover: (coverType: CoverType, cover: string) => void;
onUpdateIcon: (icon: string) => void;
}
function ViewIconGroup({ icon, onUpdateIcon }: Props) {
const { t } = useTranslation();
const showAddIcon = !icon;
const onAddIcon = useCallback(() => {
const emoji = randomEmoji();
onUpdateIcon(emoji);
}, [onUpdateIcon]);
// const onAddCover = useCallback(() => {
// const color = randomColor();
//
// onUpdateCover(CoverType.Color, color);
// }, []);
return (
<div className={'flex items-center py-2'}>
{showAddIcon && (
<Button onClick={onAddIcon} color={'inherit'} startIcon={<EmojiEmotionsOutlined />}>
{t('document.plugins.cover.addIcon')}
</Button>
)}
{/*{showAddCover && (*/}
{/* <Button onClick={onAddCover} color={'inherit'} startIcon={<ImageOutlined />}>*/}
{/* {t('document.plugins.cover.addCover')}*/}
{/* </Button>*/}
{/*)}*/}
</div>
);
}
export default ViewIconGroup;

View File

@ -0,0 +1,64 @@
import React, { FormEventHandler, useCallback, useEffect, useRef, useState } from 'react';
import ViewBanner from '$app/components/_shared/ViewTitle/ViewBanner';
import { Page, PageIcon } from '$app_reducers/pages/slice';
import { ViewIconTypePB } from '@/services/backend';
import { TextareaAutosize } from '@mui/material';
import { useTranslation } from 'react-i18next';
interface Props {
view: Page;
onTitleChange: (title: string) => void;
onUpdateIcon: (icon: PageIcon) => void;
}
function ViewTitle({ view, onTitleChange: onTitleChangeProp, onUpdateIcon: onUpdateIconProp }: Props) {
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [hover, setHover] = useState(false);
const [icon, setIcon] = useState<PageIcon | undefined>(view.icon);
const defaultValue = useRef(view.name);
const onTitleChange: FormEventHandler<HTMLTextAreaElement> = (e) => {
const value = e.currentTarget.value;
onTitleChangeProp(value);
};
const onUpdateIcon = useCallback(
(icon: string) => {
const newIcon = {
value: icon,
ty: ViewIconTypePB.Emoji,
};
setIcon(newIcon);
onUpdateIconProp(newIcon);
},
[onUpdateIconProp]
);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.focus();
// set the cursor to the end of the text
textareaRef.current.setSelectionRange(textareaRef.current.value.length, textareaRef.current.value.length);
}
}, []);
return (
<div className={'flex flex-col'} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
<ViewBanner icon={icon} hover={hover} onUpdateIcon={onUpdateIcon} />
<div className='relative'>
<TextareaAutosize
ref={textareaRef}
placeholder={t('document.title.placeholder')}
className='min-h-[40px] resize-none text-4xl font-bold caret-text-title'
autoCorrect='off'
defaultValue={defaultValue.current}
onInput={onTitleChange}
/>
</div>
</div>
);
}
export default ViewTitle;

View File

@ -4,18 +4,14 @@ export interface CellTextProps {
className?: string;
}
export const CellText = React.forwardRef<HTMLDivElement, PropsWithChildren<HTMLAttributes<HTMLDivElement>>>(function CellText(props, ref) {
const { children, className, ...other } = props;
export const CellText = React.forwardRef<HTMLDivElement, PropsWithChildren<HTMLAttributes<HTMLDivElement>>>(
function CellText(props, ref) {
const { children, className, ...other } = props;
return (
<div
ref={ref}
className={['flex p-2', className].join(' ')}
{...other}
>
<span className="flex-1 text-sm truncate">
{children}
</span>
</div>
);
});
return (
<div ref={ref} className={['flex w-full p-2', className].join(' ')} {...other}>
<span className='flex-1 truncate text-sm'>{children}</span>
</div>
);
}
);

View File

@ -25,7 +25,13 @@ export async function getCell(viewId: string, rowId: string, fieldId: string, fi
const result = await DatabaseEventGetCell(payload);
return result.map(value => pbToCell(value, fieldType)).unwrap();
if (result.ok === false) {
return Promise.reject(result.val);
}
const value = result.val;
return pbToCell(value, fieldType);
}
export async function updateCell(viewId: string, rowId: string, fieldId: string, changeset: string): Promise<void> {
@ -48,7 +54,7 @@ export async function updateSelectCell(
data: {
insertOptionIds?: string[];
deleteOptionIds?: string[];
},
}
): Promise<void> {
const payload = SelectOptionCellChangesetPB.fromObject({
cell_identifier: {
@ -74,7 +80,7 @@ export async function updateChecklistCell(
selectedOptionIds?: string[];
deleteOptionIds?: string[];
updateOptions?: Partial<SelectOption>[];
},
}
): Promise<void> {
const payload = ChecklistCellDataChangesetPB.fromObject({
view_id: viewId,
@ -100,7 +106,7 @@ export async function updateDateCell(
time?: string;
includeTime?: boolean;
clearFlag?: boolean;
},
}
): Promise<void> {
const payload = DateChangesetPB.fromObject({
cell_id: {

View File

@ -10,6 +10,9 @@ import { CheckboxCell } from './CheckboxCell';
export interface CellProps {
rowId: string;
field: Field;
documentId?: string;
icon?: string;
placeholder?: string;
}
const getCellComponent = (fieldType: FieldType) => {
@ -26,7 +29,7 @@ const getCellComponent = (fieldType: FieldType) => {
}
};
export const Cell: FC<CellProps> = ({ rowId, field }) => {
export const Cell: FC<CellProps> = ({ rowId, field, ...props }) => {
const cell = useCell(rowId, field.id, field.type);
const Component = getCellComponent(field.type);
@ -35,5 +38,5 @@ export const Cell: FC<CellProps> = ({ rowId, field }) => {
return null;
}
return <Component field={field} cell={cell} />;
return <Component {...props} field={field} cell={cell} />;
};

View File

@ -0,0 +1,52 @@
import React, { useEffect, useRef } from 'react';
import { Popover, TextareaAutosize } from '@mui/material';
interface Props {
editing: boolean;
anchorEl: HTMLDivElement | null;
width: number | undefined;
onClose: () => void;
text: string;
onInput: (event: React.FormEvent<HTMLTextAreaElement>) => void;
}
function EditTextCellInput({ editing, anchorEl, width, onClose, text, onInput }: Props) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.focus();
// set the cursor to the end of the text
const length = textareaRef.current.value.length;
textareaRef.current.setSelectionRange(length, length);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textareaRef.current]);
return (
<Popover
open={editing}
anchorEl={anchorEl}
PaperProps={{
className: 'flex p-2 border border-blue-400',
style: { width, borderRadius: 0, boxShadow: 'none' },
}}
transformOrigin={{
vertical: 1,
horizontal: 'left',
}}
transitionDuration={0}
onClose={onClose}
>
<TextareaAutosize
ref={textareaRef}
className='resize-none text-sm'
autoFocus
autoCorrect='off'
value={text}
onInput={onInput}
/>
</Popover>
);
}
export default EditTextCellInput;

View File

@ -0,0 +1,38 @@
import React, { useState } from 'react';
import { TextCell } from '$app/components/database/application';
import { IconButton } from '@mui/material';
import { ReactComponent as OpenIcon } from '$app/assets/open.svg';
const ExpandCellModal = React.lazy(() => import('$app/components/database/components/cell/expand_type/ExpandCellModal'));
interface Props {
cell: TextCell;
visible?: boolean;
documentId?: string;
icon?: string;
}
function ExpandButton({ cell, documentId, icon, visible }: Props) {
const [open, setOpen] = useState(false);
const onClose = () => {
setOpen(false);
};
return (
<>
{visible && (
<div className={`mr-4 flex items-center justify-center`}>
<IconButton onClick={() => setOpen(true)} className={'h-6 w-6 text-sm'}>
<OpenIcon />
</IconButton>
</div>
)}
{open && documentId && (
<ExpandCellModal documentId={documentId} icon={icon} cell={cell} open={open} onClose={onClose} />
)}
</>
);
}
export default ExpandButton;

View File

@ -1,20 +1,31 @@
import { Popover, TextareaAutosize } from '@mui/material';
import { FC, FormEventHandler, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { FC, FormEventHandler, Suspense, lazy, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useViewId } from '$app/hooks';
import { cellService, Field, TextCell as TextCellType } from '../../application';
import { CellText } from '../../_shared';
import { useGridUIStateDispatcher } from '$app/components/database/proxy/grid/ui_state/actions';
import { useGridUIStateDispatcher, useGridUIStateSelector } from '$app/components/database/proxy/grid/ui_state/actions';
const ExpandButton = lazy(() => import('$app/components/database/components/cell/ExpandButton'));
const EditTextCellInput = lazy(() => import('$app/components/database/components/cell/EditTextCellInput'));
export const TextCell: FC<{
field: Field;
cell?: TextCellType;
}> = ({ field, cell }) => {
documentId?: string;
icon?: string;
placeholder?: string;
}> = ({ field, cell, documentId, icon, placeholder }) => {
const isPrimary = field.isPrimary;
const viewId = useViewId();
const cellRef = useRef<HTMLDivElement>(null);
const [editing, setEditing] = useState(false);
const [text, setText] = useState('');
const [width, setWidth] = useState<number | undefined>(undefined);
const { hoverRowId } = useGridUIStateSelector();
const isHover = hoverRowId === cell?.rowId;
const { setRowHover } = useGridUIStateDispatcher();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const showExpandIcon = cell && !editing && isHover && isPrimary;
const handleClose = () => {
if (!cell) return;
if (editing) {
@ -48,35 +59,35 @@ export const TextCell: FC<{
}
}, [editing, setRowHover]);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.focus();
// set the cursor to the end of the text
textareaRef.current.setSelectionRange(textareaRef.current.value.length, textareaRef.current.value.length);
}
}, []);
return (
<>
<CellText ref={cellRef} className='w-full' onClick={handleClick}>
{cell?.data}
<CellText ref={cellRef} onClick={handleClick}>
<div className='flex w-full items-center'>
{icon && <div className={'mr-2'}>{icon}</div>}
{cell?.data || <div className={'text-text-placeholder'}>{placeholder}</div>}
</div>
</CellText>
{editing && (
<Popover
open={editing}
anchorEl={cellRef.current}
PaperProps={{
className: 'flex p-2 border border-blue-400',
style: { width, borderRadius: 0, boxShadow: 'none' },
}}
transformOrigin={{
vertical: 1,
horizontal: 'left',
}}
transitionDuration={0}
onClose={handleClose}
>
<TextareaAutosize
className='resize-none text-sm'
autoFocus
autoCorrect='off'
value={text}
<Suspense>
{cell && <ExpandButton visible={showExpandIcon} icon={icon} documentId={documentId} cell={cell} />}
{editing && (
<EditTextCellInput
editing={editing}
anchorEl={cellRef.current}
width={width}
onClose={handleClose}
text={text}
onInput={handleInput}
/>
</Popover>
)}
)}
</Suspense>
</>
);
};

View File

@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { DialogProps, IconButton } from '@mui/material';
import DialogContent from '@mui/material/DialogContent';
import Dialog from '@mui/material/Dialog';
import { TextCell } from '$app/components/database/application';
import { ReactComponent as DetailsIcon } from '$app/assets/details.svg';
import RecordActions from '$app/components/database/components/edit_record/RecordActions';
import EditRecord from '$app/components/database/components/edit_record/EditRecord';
interface Props extends DialogProps {
cell: TextCell;
documentId: string;
icon?: string;
}
function ExpandCellModal({ open, onClose, cell, documentId, icon }: Props) {
const [detailAnchorEl, setDetailAnchorEl] = useState<HTMLButtonElement | null>(null);
return (
<>
<Dialog
disableAutoFocus={true}
keepMounted={false}
onMouseDown={(e) => e.stopPropagation()}
open={open}
onClose={onClose}
PaperProps={{
className: 'h-[calc(100%-144px)] w-[80%] max-w-[960px]',
}}
>
<IconButton
aria-label='close'
className={'absolute right-[8px] top-[8px] text-text-caption'}
onClick={(e) => {
setDetailAnchorEl(e.currentTarget);
}}
>
<DetailsIcon />
</IconButton>
<DialogContent>
<EditRecord cell={cell} documentId={documentId} icon={icon} />
</DialogContent>
</Dialog>
<RecordActions
anchorEl={detailAnchorEl}
cell={cell}
open={!!detailAnchorEl}
onClose={() => setDetailAnchorEl(null)}
/>
</>
);
}
export default ExpandCellModal;

View File

@ -0,0 +1,55 @@
import React, { useCallback, useEffect, useState } from 'react';
import { TextCell } from '$app/components/database/application';
import RecordDocument from '$app/components/database/components/edit_record/RecordDocument';
import RecordHeader from '$app/components/database/components/edit_record/RecordHeader';
import { Page } from '$app_reducers/pages/slice';
import { PageController } from '$app/stores/effects/workspace/page/page_controller';
import { ViewLayoutPB } from '@/services/backend';
interface Props {
cell: TextCell;
documentId: string;
icon?: string;
}
function EditRecord({ documentId: id, cell, icon }: Props) {
const [page, setPage] = useState<Page | null>(null);
const loadPage = useCallback(async () => {
if (!id) return;
const controller = new PageController(id);
try {
const page = await controller.getPage();
setPage(page);
} catch (e) {
// Record not found
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (e.code === 3) {
const page = await controller.createOrphanPage({
name: '',
layout: ViewLayoutPB.Document,
});
setPage(page);
}
}
}, [id]);
useEffect(() => {
void loadPage();
}, [loadPage]);
const getDocumentTitle = useCallback(() => {
return <RecordHeader page={page} cell={cell} icon={icon} />;
}, [cell, icon, page]);
return (
<div className={'h-full px-12 py-6'}>
<RecordDocument getDocumentTitle={getDocumentTitle} documentId={id} />
</div>
);
}
export default React.memo(EditRecord);

View File

@ -0,0 +1,60 @@
import React, { useCallback } from 'react';
import { Icon, Menu, MenuProps } from '@mui/material';
import { ReactComponent as DelSvg } from '$app/assets/delete.svg';
import { ReactComponent as CopySvg } from '$app/assets/copy.svg';
import { useTranslation } from 'react-i18next';
import { Cell, rowService } from '$app/components/database/application';
import { useViewId } from '$app/hooks';
import MenuItem from '@mui/material/MenuItem';
interface Props extends MenuProps {
cell: Cell;
onClose?: () => void;
}
function RecordActions({ anchorEl, open, onClose, cell }: Props) {
const viewId = useViewId();
const rowId = cell.rowId;
const { t } = useTranslation();
const handleDelRow = useCallback(() => {
void rowService.deleteRow(viewId, rowId);
}, [viewId, rowId]);
const handleDuplicateRow = useCallback(() => {
void rowService.duplicateRow(viewId, rowId);
}, [viewId, rowId]);
const menuOptions = [
{
label: t('grid.row.duplicate'),
icon: <CopySvg />,
onClick: handleDuplicateRow,
},
{
label: t('grid.row.delete'),
icon: <DelSvg />,
onClick: handleDelRow,
divider: true,
},
];
return (
<Menu anchorEl={anchorEl} open={open} onClose={onClose}>
{menuOptions.map((option) => (
<MenuItem
key={option.label}
onClick={() => {
option.onClick();
onClose?.();
}}
>
<Icon className='mr-2'>{option.icon}</Icon>
{option.label}
</MenuItem>
))}
</Menu>
);
}
export default RecordActions;

View File

@ -0,0 +1,18 @@
import React from 'react';
import Document from '$app/components/document';
import { ContainerType } from '$app/hooks/document.hooks';
interface Props {
documentId: string;
getDocumentTitle?: () => React.ReactNode;
}
function RecordDocument({ documentId, getDocumentTitle }: Props) {
return (
<div className={'-ml-[72px] h-full min-h-[200px] w-[calc(100%+144px)]'}>
<Document getDocumentTitle={getDocumentTitle} containerType={ContainerType.EditRecord} documentId={documentId} />
</div>
);
}
export default React.memo(RecordDocument);

View File

@ -0,0 +1,40 @@
import React, { useEffect, useRef } from 'react';
import RecordTitle from '$app/components/database/components/edit_record/RecordTitle';
import RecordProperties from '$app/components/database/components/edit_record/record_properties/RecordProperties';
import { Divider } from '@mui/material';
import { TextCell } from '$app/components/database/application';
import { Page } from '$app_reducers/pages/slice';
interface Props {
page: Page | null;
cell: TextCell;
icon?: string;
}
function RecordHeader({ page, cell, icon }: Props) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const preventSelectionTrigger = (e: MouseEvent) => {
e.stopPropagation();
};
el.addEventListener('mousedown', preventSelectionTrigger);
return () => {
el.removeEventListener('mousedown', preventSelectionTrigger);
};
}, []);
return (
<div ref={ref} className={'pb-4'}>
<RecordTitle page={page} cell={cell} icon={icon} />
<RecordProperties documentId={page?.id} cell={cell} />
<Divider />
</div>
);
}
export default RecordHeader;

View File

@ -0,0 +1,63 @@
import React, { useCallback } from 'react';
import { Page, PageIcon } from '$app_reducers/pages/slice';
import ViewTitle from '$app/components/_shared/ViewTitle';
import { ViewIconTypePB } from '@/services/backend';
import { useViewId } from '$app/hooks';
import { updateRowMeta } from '$app/components/database/application/row/row_service';
import { cellService, TextCell } from '$app/components/database/application';
interface Props {
page: Page | null;
icon?: string;
cell: TextCell;
}
function RecordTitle({ cell, page, icon }: Props) {
const { data: title, fieldId, rowId } = cell;
const viewId = useViewId();
const onTitleChange = useCallback(
async (title: string) => {
try {
await cellService.updateCell(viewId, rowId, fieldId, title);
} catch (e) {
// toast.error('Failed to update title');
}
},
[fieldId, rowId, viewId]
);
const onUpdateIcon = useCallback(
async (icon: PageIcon) => {
try {
await updateRowMeta(viewId, rowId, { iconUrl: icon.value });
} catch (e) {
// toast.error('Failed to update icon');
}
},
[rowId, viewId]
);
return (
<div className={'px-1 pb-4 pt-2'}>
{page && (
<ViewTitle
onUpdateIcon={onUpdateIcon}
onTitleChange={onTitleChange}
view={{
...page,
name: title,
icon: icon
? {
ty: ViewIconTypePB.Emoji,
value: icon,
}
: undefined,
}}
/>
)}
</div>
);
}
export default React.memo(RecordTitle);

View File

@ -0,0 +1,51 @@
import React, { MouseEvent, useCallback, useState } from 'react';
import { Field, fieldService } from '$app/components/database/application';
import { FieldType } from '@/services/backend';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
import { useViewId } from '$app/hooks';
import { FieldMenu } from '$app/components/database/components/field/FieldMenu';
function NewProperty() {
const viewId = useViewId();
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const [updateField, setUpdateField] = useState<Field | null>(null);
const handleClick = useCallback(
async (e: MouseEvent<HTMLButtonElement>) => {
try {
const field = await fieldService.createField(viewId, FieldType.RichText);
setUpdateField(field);
setAnchorEl(e.target as HTMLButtonElement);
} catch (e) {
// toast.error(t('grid.field.newPropertyFail'));
}
},
[viewId]
);
return (
<>
<Button onClick={handleClick} className={'h-full w-full justify-start'} startIcon={<AddSvg />} color={'inherit'}>
{t('grid.field.newProperty')}
</Button>
{updateField && (
<FieldMenu
field={updateField}
anchorEl={anchorEl}
open={open}
onClose={() => {
setUpdateField(null);
setAnchorEl(null);
}}
/>
)}
</>
);
}
export default NewProperty;

View File

@ -0,0 +1,47 @@
import React, { HTMLAttributes, useCallback, useState } from 'react';
import PropertyName from '$app/components/database/components/edit_record/record_properties/PropertyName';
import PropertyValue from '$app/components/database/components/edit_record/record_properties/PropertyValue';
import { Field } from '$app/components/database/application';
import PropertyActions from '$app/components/database/components/edit_record/record_properties/PropertyActions';
interface Props extends HTMLAttributes<HTMLDivElement> {
field: Field;
rowId: string;
ishovered: boolean;
onHover: (id: string | null) => void;
}
function Property({ field, rowId, ishovered, onHover, ...props }: Props, ref: React.ForwardedRef<HTMLDivElement>) {
const [openMenu, setOpenMenu] = useState(false);
const handleOpenMenu = useCallback(() => {
setOpenMenu(true);
}, []);
const handleCloseMenu = useCallback(() => {
setOpenMenu(false);
}, []);
return (
<>
<div
ref={ref}
onMouseEnter={() => {
onHover(field.id);
}}
onMouseLeave={() => {
onHover(null);
}}
className={'relative flex gap-6 rounded hover:bg-content-blue-50'}
key={field.id}
{...props}
>
<PropertyName openMenu={openMenu} onOpenMenu={handleOpenMenu} onCloseMenu={handleCloseMenu} field={field} />
<PropertyValue rowId={rowId} field={field} />
{ishovered && <PropertyActions onOpenMenu={handleOpenMenu} />}
</div>
</>
);
}
export default React.memo(React.forwardRef(Property));

View File

@ -0,0 +1,20 @@
import React, { forwardRef } from 'react';
import { t } from 'i18next';
import { IconButton, Tooltip } from '@mui/material';
import { ReactComponent as DragSvg } from '$app/assets/drag.svg';
interface Props {
onOpenMenu: () => void;
}
export default forwardRef<HTMLDivElement, Props>(function PropertyActions({ onOpenMenu }, ref) {
return (
<div ref={ref} className={`absolute left-[-30px] flex h-full items-center `}>
<Tooltip placement='top' title={t('grid.row.dragAndClick')}>
<IconButton onClick={onOpenMenu} className='mx-1 cursor-grab active:cursor-grabbing'>
<DragSvg />
</IconButton>
</Tooltip>
</div>
);
});

View File

@ -0,0 +1,58 @@
import React, { HTMLAttributes, useState } from 'react';
import { Field } from '$app/components/database/application';
import Property from '$app/components/database/components/edit_record/record_properties/Property';
import { Draggable } from 'react-beautiful-dnd';
interface Props extends HTMLAttributes<HTMLDivElement> {
documentId?: string;
properties: Field[];
rowId: string;
placeholderNode?: React.ReactNode;
}
function PropertyList(
{ documentId, properties, rowId, placeholderNode, ...props }: Props,
ref: React.ForwardedRef<HTMLDivElement>
) {
const [hoverId, setHoverId] = useState<string | null>(null);
return (
<div ref={ref} {...props} className={'flex w-full flex-col pb-3 pt-2'}>
{properties.map((field, index) => {
return (
<Draggable key={field.id} draggableId={field.id} index={index}>
{(provided) => {
let top;
if (provided.draggableProps.style && 'top' in provided.draggableProps.style) {
const scrollContainer = document.querySelector(`#appflowy-scroller_${documentId}`);
top = provided.draggableProps.style.top - 113 + (scrollContainer?.scrollTop || 0);
}
return (
<Property
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...provided.draggableProps.style,
left: 'auto !important',
top: top !== undefined ? top : undefined,
}}
onHover={setHoverId}
ishovered={field.id === hoverId}
field={field}
rowId={rowId}
/>
);
}}
</Draggable>
);
})}
{placeholderNode}
</div>
);
}
export default React.memo(React.forwardRef(PropertyList));

View File

@ -0,0 +1,34 @@
import React, { useRef } from 'react';
import { Field } from '$app/components/database/components/field';
import { Field as FieldType } from '$app/components/database/application';
import { FieldMenu } from '$app/components/database/components/field/FieldMenu';
interface Props {
field: FieldType;
openMenu: boolean;
onOpenMenu: () => void;
onCloseMenu: () => void;
}
function PropertyName({ field, openMenu, onOpenMenu, onCloseMenu }: Props) {
const ref = useRef<HTMLDivElement | null>(null);
return (
<>
<div
ref={ref}
onContextMenu={(e) => {
e.stopPropagation();
e.preventDefault();
onOpenMenu();
}}
className={'flex w-[200px] cursor-pointer items-center'}
onClick={onOpenMenu}
>
<Field field={field} />
</div>
{openMenu && <FieldMenu field={field} open={openMenu} anchorEl={ref.current} onClose={onCloseMenu} />}
</>
);
}
export default React.memo(PropertyName);

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Cell } from '$app/components/database/components';
import { Field } from '$app/components/database/application';
import { useTranslation } from 'react-i18next';
function PropertyValue(props: { rowId: string; field: Field }) {
const { t } = useTranslation();
return (
<div className={'flex h-9 flex-1 items-center'}>
<Cell placeholder={t('grid.row.textPlaceholder')} {...props} />
</div>
);
}
export default React.memo(PropertyValue);

View File

@ -0,0 +1,121 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Field, fieldService, TextCell } from '$app/components/database/application';
import { useDatabase } from '$app/components/database';
import { FieldVisibility } from '@/services/backend';
import Button from '@mui/material/Button';
import { ReactComponent as EyeClosedSvg } from '$app/assets/eye_close.svg';
import { ReactComponent as EyeOpenSvg } from '$app/assets/eye_open.svg';
import PropertyList from '$app/components/database/components/edit_record/record_properties/PropertyList';
import NewProperty from '$app/components/database/components/edit_record/record_properties/NewProperty';
import { useViewId } from '$app/hooks';
import { DragDropContext, Droppable, DropResult, OnDragEndResponder } from 'react-beautiful-dnd';
interface Props {
documentId?: string;
cell: TextCell;
}
// a little function to help us with reordering the result
const reorder = (list: Field[], startIndex: number, endIndex: number) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
function RecordProperties({ documentId, cell }: Props) {
const viewId = useViewId();
const { fieldId, rowId } = cell;
const { fields } = useDatabase();
const [showHiddenFields, setShowHiddenFields] = useState(false);
const properties = useMemo(() => {
return fields.filter((field) => {
// exclude the current field, because it's already displayed in the title
// filter out hidden fields if the user doesn't want to see them
return field.id !== fieldId && (showHiddenFields || field.visibility !== FieldVisibility.AlwaysHidden);
});
}, [fieldId, fields, showHiddenFields]);
const [state, setState] = useState<Field[]>(properties);
// move the field in the database
const onMoveProperty = useCallback(
async (fieldId: string, prevId?: string) => {
const fromIndex = fields.findIndex((field) => field.id === fieldId);
const prevIndex = prevId ? fields.findIndex((field) => field.id === prevId) : 0;
const toIndex = prevIndex > fromIndex ? prevIndex : prevIndex + 1;
if (fromIndex === toIndex) {
return;
}
await fieldService.moveField(viewId, fieldId, fromIndex, toIndex);
},
[fields, viewId]
);
// move the field in the state
const handleOnDragEnd: OnDragEndResponder = useCallback(
async (result: DropResult) => {
const { destination, draggableId, source } = result;
const newIndex = destination?.index;
const oldIndex = source.index;
if (newIndex === undefined || newIndex === null) {
return;
}
// reorder the properties synchronously to avoid flickering
const newProperties = reorder(properties, oldIndex, newIndex ?? 0);
setState(newProperties);
// find the previous field id
const prevIndex = newProperties.findIndex((field) => field.id === draggableId) - 1;
const prevId = prevIndex >= 0 ? newProperties[prevIndex].id : undefined;
// update the order in the database.
// why not prevIndex? because the properties was filtered, we need to use the previous id to find the correct index
await onMoveProperty(draggableId, prevId);
},
[onMoveProperty, properties]
);
return (
<div className={'relative flex w-full flex-col pb-4'}>
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId='droppable' type='droppableItem'>
{(dropProvided) => (
<PropertyList
documentId={documentId}
placeholderNode={dropProvided.placeholder}
ref={dropProvided.innerRef}
{...dropProvided.droppableProps}
rowId={rowId}
properties={state}
/>
)}
</Droppable>
</DragDropContext>
<Button
onClick={() => {
setShowHiddenFields((prev) => !prev);
}}
className={'w-full justify-start'}
startIcon={showHiddenFields ? <EyeClosedSvg /> : <EyeOpenSvg />}
color={'inherit'}
>
{showHiddenFields ? 'Hide hidden fields' : 'Show hidden fields'}
</Button>
<NewProperty />
</div>
);
}
export default React.memo(RecordProperties);

View File

@ -8,7 +8,7 @@ export interface FieldsMenuProps extends MenuProps {
onMenuItemClick?: (event: MouseEvent<HTMLLIElement>, field: FieldType) => void;
}
export const FieldsMenu: FC<FieldsMenuProps> = ({ onMenuItemClick, ...props }) => {
export const FieldListMenu: FC<FieldsMenuProps> = ({ onMenuItemClick, ...props }) => {
const fields = useDatabaseVisibilityFields();
return (

View File

@ -3,9 +3,8 @@ import { ChangeEventHandler, FC, useCallback, useState } from 'react';
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
import { useViewId } from '$app/hooks';
import { Field, fieldService } from '../../application';
import { FieldTypeSvg } from './FieldTypeSvg';
import { FieldTypeText } from './FieldTypeText';
import { GridFieldMenuActions } from './GridFieldMenuActions';
import { FieldMenuActions } from './FieldMenuActions';
import { FieldTypeText, FieldTypeSvg } from '$app/components/database/components/field/index';
export interface GridFieldMenuProps {
field: Field;
@ -14,7 +13,7 @@ export interface GridFieldMenuProps {
onClose: () => void;
}
export const GridFieldMenu: FC<GridFieldMenuProps> = ({ field, anchorEl, open, onClose }) => {
export const FieldMenu: FC<GridFieldMenuProps> = ({ field, anchorEl, open, onClose }) => {
const viewId = useViewId();
const [inputtingName, setInputtingName] = useState(field.name);
@ -48,7 +47,9 @@ export const GridFieldMenu: FC<GridFieldMenuProps> = ({ field, anchorEl, open, o
const fieldTypeSelect = (
<MenuItem dense>
<FieldTypeSvg type={field.type} className='mr-2 text-base' />
<span className='flex-1 text-xs font-medium'>{FieldTypeText(field.type)}</span>
<span className='flex-1 text-xs font-medium'>
<FieldTypeText type={field.type} />
</span>
<MoreSvg className='text-base' />
</MenuItem>
);
@ -56,18 +57,16 @@ export const GridFieldMenu: FC<GridFieldMenuProps> = ({ field, anchorEl, open, o
const isPrimary = field.isPrimary;
return (
<>
<Menu anchorEl={anchorEl} open={open} onClose={onClose}>
{fieldNameInput}
{!isPrimary && (
<>
{fieldTypeSelect}
<Divider />
</>
)}
<Menu keepMounted={false} anchorEl={anchorEl} open={open} onClose={onClose}>
{fieldNameInput}
{!isPrimary && (
<div>
{fieldTypeSelect}
<Divider />
</div>
)}
<GridFieldMenuActions isPrimary={isPrimary} onMenuItemClick={() => onClose()} fieldId={field.id} />
</Menu>
</>
<FieldMenuActions isPrimary={isPrimary} onMenuItemClick={() => onClose()} fieldId={field.id} />
</Menu>
);
};

View File

@ -42,7 +42,7 @@ interface GridFieldMenuActionsProps {
onMenuItemClick?: (action: FieldAction) => void;
}
export const GridFieldMenuActions = ({ fieldId, onMenuItemClick, isPrimary }: GridFieldMenuActionsProps) => {
export const FieldMenuActions = ({ fieldId, onMenuItemClick, isPrimary }: GridFieldMenuActionsProps) => {
const viewId = useViewId();
const [openConfirm, setOpenConfirm] = useState(false);

View File

@ -1,8 +1,7 @@
import { Divider, Menu, MenuItem, MenuProps } from '@mui/material';
import { FC, useMemo } from 'react';
import { FieldType } from '@/services/backend';
import { FieldTypeSvg } from './FieldTypeSvg';
import { FieldTypeText } from './FieldTypeText';
import { FieldTypeText, FieldTypeSvg } from '$app/components/database/components/field/index';
const FieldTypeGroup = [
{
@ -19,33 +18,35 @@ const FieldTypeGroup = [
},
{
name: 'Advanced',
types: [
FieldType.LastEditedTime,
],
types: [FieldType.LastEditedTime],
},
];
export const FieldTypeMenu: FC<MenuProps> = (props) => {
const PopoverClasses = useMemo(() => ({
...props.PopoverClasses,
paper: ['w-56', props.PopoverClasses?.paper].join(' '),
}), [props.PopoverClasses]);
const PopoverClasses = useMemo(
() => ({
...props.PopoverClasses,
paper: ['w-56', props.PopoverClasses?.paper].join(' '),
}),
[props.PopoverClasses]
);
return (
<Menu
{...props}
PopoverClasses={PopoverClasses}
>
<Menu {...props} PopoverClasses={PopoverClasses}>
{FieldTypeGroup.map((group, index) => [
<MenuItem key={group.name} dense disabled>{group.name}</MenuItem>,
group.types.map(type => (
<MenuItem key={group.name} dense disabled>
{group.name}
</MenuItem>,
group.types.map((type) => (
<MenuItem key={type} dense>
<FieldTypeSvg className="mr-2 text-base" type={type} />
<span className="font-medium">{FieldTypeText(type)}</span>
<FieldTypeSvg className='mr-2 text-base' type={type} />
<span className='font-medium'>
<FieldTypeText type={type} />
</span>
</MenuItem>
)),
index < FieldTypeGroup.length - 1 && <Divider key={`Divider-${group.name}`} />,
])}
</Menu>
);
};
};

View File

@ -1,19 +1,39 @@
import { t } from 'i18next';
import { FieldType } from '@/services/backend';
import { useTranslation } from 'react-i18next';
import { useCallback } from 'react';
export const FieldTypeTextMap = {
[FieldType.RichText]: 'textFieldName',
[FieldType.Number]: 'numberFieldName',
[FieldType.DateTime]: 'dateFieldName',
[FieldType.SingleSelect]: 'singleSelectFieldName',
[FieldType.MultiSelect]: 'multiSelectFieldName',
[FieldType.Checkbox]: 'checkboxFieldName',
[FieldType.URL]: 'urlFieldName',
[FieldType.Checklist]: 'checklistFieldName',
[FieldType.LastEditedTime]: 'updatedAtFieldName',
[FieldType.CreatedTime]: 'createdAtFieldName',
} as const;
export const FieldTypeText = ({ type }: { type: FieldType }) => {
const { t } = useTranslation();
export const FieldTypeText = (type: FieldType) => {
return t(`grid.field.${FieldTypeTextMap[type]}`);
const getText = useCallback(
(type: FieldType) => {
switch (type) {
case FieldType.RichText:
return t('grid.field.textFieldName');
case FieldType.Number:
return t('grid.field.numberFieldName');
case FieldType.DateTime:
return t('grid.field.dateFieldName');
case FieldType.SingleSelect:
return t('grid.field.singleSelectFieldName');
case FieldType.MultiSelect:
return t('grid.field.multiSelectFieldName');
case FieldType.Checkbox:
return t('grid.field.checkboxFieldName');
case FieldType.URL:
return t('grid.field.urlFieldName');
case FieldType.Checklist:
return t('grid.field.checklistFieldName');
case FieldType.LastEditedTime:
return t('grid.field.updatedAtFieldName');
case FieldType.CreatedTime:
return t('grid.field.createdAtFieldName');
default:
return '';
}
},
[t]
);
return <>{getText(type)}</>;
};

View File

@ -1,3 +1,5 @@
export * from './Field';
export * from './FieldSelect';
export * from './FieldsMenu';
export * from './FieldListMenu';
export * from './FieldTypeText';
export * from './FieldTypeSvg';

View File

@ -3,7 +3,7 @@ import { FC, MouseEventHandler, useCallback, useState, MouseEvent } from 'react'
import { useViewId } from '$app/hooks';
import { Field, sortService } from '../../application';
import { useDatabase } from '../../Database.hooks';
import { FieldsMenu } from '../field';
import { FieldListMenu } from '../field';
import { SortItem } from './SortItem';
import { SortConditionPB } from '@/services/backend';
@ -12,7 +12,7 @@ export const SortMenu: FC<MenuProps> = (props) => {
const viewId = useViewId();
const { sorts } = useDatabase();
const [ anchorEl, setAnchorEl ] = useState<HTMLElement | null>(null);
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const handleClick = useCallback<MouseEventHandler<HTMLElement>>((event) => {
setAnchorEl(event.currentTarget);
@ -27,33 +27,27 @@ export const SortMenu: FC<MenuProps> = (props) => {
onClose?.({}, 'backdropClick');
}, [viewId, onClose]);
const addSort = useCallback((event: MouseEvent, field: Field) => {
void sortService.insertSort(viewId, {
fieldId: field.id,
fieldType: field.type,
condition: SortConditionPB.Ascending,
});
}, [viewId]);
const addSort = useCallback(
(event: MouseEvent, field: Field) => {
void sortService.insertSort(viewId, {
fieldId: field.id,
fieldType: field.type,
condition: SortConditionPB.Ascending,
});
},
[viewId]
);
return (
<>
<Menu {...props}>
{sorts.map(sort => (
<SortItem key={sort.id} className="mx-2" sort={sort} />
{sorts.map((sort) => (
<SortItem key={sort.id} className='mx-2' sort={sort} />
))}
<MenuItem onClick={handleClick}>
Add sort
</MenuItem>
<MenuItem onClick={deleteAllSorts}>
Delete sort
</MenuItem>
<MenuItem onClick={handleClick}>Add sort</MenuItem>
<MenuItem onClick={deleteAllSorts}>Delete sort</MenuItem>
</Menu>
<FieldsMenu
open={anchorEl !== null}
anchorEl={anchorEl}
onClose={handleClose}
onMenuItemClick={addSort}
/>
<FieldListMenu open={anchorEl !== null} anchorEl={anchorEl} onClose={handleClose} onMenuItemClick={addSort} />
</>
);
};

View File

@ -1,30 +0,0 @@
import { FC } from 'react';
import { FieldType } from '@/services/backend';
import { ReactComponent as TextSvg } from '$app/assets/database/field-type-text.svg';
import { ReactComponent as NumberSvg } from '$app/assets/database/field-type-number.svg';
import { ReactComponent as DateSvg } from '$app/assets/database/field-type-date.svg';
import { ReactComponent as SingleSelectSvg } from '$app/assets/database/field-type-single-select.svg';
import { ReactComponent as MultiSelectSvg } from '$app/assets/database/field-type-multi-select.svg';
import { ReactComponent as ChecklistSvg } from '$app/assets/database/field-type-checklist.svg';
import { ReactComponent as CheckboxSvg } from '$app/assets/database/field-type-checkbox.svg';
import { ReactComponent as URLSvg } from '$app/assets/database/field-type-url.svg';
import { ReactComponent as LastEditedTimeSvg } from '$app/assets/database/field-type-last-edited-time.svg';
export const FieldTypeSvgMap: Record<FieldType, FC<{ className?: string }>> = {
[FieldType.RichText]: TextSvg,
[FieldType.Number]: NumberSvg,
[FieldType.DateTime]: DateSvg,
[FieldType.SingleSelect]: SingleSelectSvg,
[FieldType.MultiSelect]: MultiSelectSvg,
[FieldType.Checkbox]: CheckboxSvg,
[FieldType.URL]: URLSvg,
[FieldType.Checklist]: ChecklistSvg,
[FieldType.LastEditedTime]: LastEditedTimeSvg,
[FieldType.CreatedTime]: LastEditedTimeSvg,
};
export const FieldTypeSvg: FC<{ type: FieldType; className?: string }> = ({ type, ...props }) => {
const Svg = FieldTypeSvgMap[type];
return <Svg {...props} />;
};

View File

@ -1,19 +0,0 @@
import { t } from 'i18next';
import { FieldType } from '@/services/backend';
export const FieldTypeTextMap = {
[FieldType.RichText]: 'textFieldName',
[FieldType.Number]: 'numberFieldName',
[FieldType.DateTime]: 'dateFieldName',
[FieldType.SingleSelect]: 'singleSelectFieldName',
[FieldType.MultiSelect]: 'multiSelectFieldName',
[FieldType.Checkbox]: 'checkboxFieldName',
[FieldType.URL]: 'urlFieldName',
[FieldType.Checklist]: 'checklistFieldName',
[FieldType.LastEditedTime]: 'updatedAtFieldName',
[FieldType.CreatedTime]: 'createdAtFieldName',
} as const;
export const FieldTypeText = (type: FieldType) => {
return t(`grid.field.${FieldTypeTextMap[type]}`);
}

View File

@ -5,8 +5,8 @@ import { useViewId } from '$app/hooks';
import { DragItem, DropPosition, DragType, useDraggable, useDroppable, ScrollDirection } from '../../_shared';
import { fieldService, Field } from '../../application';
import { useDatabase } from '../../Database.hooks';
import { FieldTypeSvg } from './FieldTypeSvg';
import { GridFieldMenu } from './GridFieldMenu';
import { FieldTypeSvg } from '$app/components/database/components/field';
import { FieldMenu } from '../../components/field/FieldMenu';
import GridResizer from '$app/components/database/grid/GridField/GridResizer';
import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow';
@ -134,9 +134,7 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
<GridResizer field={field} onWidthChange={(width) => setFieldWidth(width)} />
</Button>
</Tooltip>
{openMenu && (
<GridFieldMenu field={field} open={openMenu} anchorEl={previewRef.current} onClose={handleMenuClose} />
)}
{openMenu && <FieldMenu field={field} open={openMenu} anchorEl={previewRef.current} onClose={handleMenuClose} />}
</div>
);
};

View File

@ -116,7 +116,13 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
className='flex'
itemClassName='flex border-r border-line-divider'
virtualizer={virtualizer}
renderItem={(index) => <GridCell rowId={rowMeta.id} field={fields[index]} />}
renderItem={(index) => {
const field = fields[index];
const icon = field.isPrimary ? rowMeta.icon : undefined;
const documentId = field.isPrimary ? rowMeta.documentId : undefined;
return <GridCell rowId={rowMeta.id} documentId={documentId} icon={icon} field={field} />;
}}
/>
<div className={`w-[${DEFAULT_FIELD_WIDTH}px]`} />
{isOver && (

View File

@ -29,57 +29,69 @@ export const GridCellRowActions: FC<PropsWithChildren<GridCellRowActionsProps>>
}) => {
const viewId = useViewId();
const ref = useRef<HTMLDivElement | null>(null);
const [openMenu, setOpenMenu] = useState(false);
const [menuPosition, setMenuPosition] = useState<{
top: number;
left: number;
}>();
const handleInsertRecordBelow = useCallback(() => {
void rowService.createRow(viewId, {
startRowId: rowId,
});
}, [viewId, rowId]);
const handleOpenMenu = () => {
setOpenMenu(true);
const handleOpenMenu = (e: React.MouseEvent) => {
const target = e.target as HTMLButtonElement;
const rect = target.getBoundingClientRect();
setMenuPosition({
top: rect.top + rect.height / 2,
left: rect.left + rect.width,
});
};
const handleCloseMenu = () => {
setOpenMenu(false);
setMenuPosition(undefined);
};
if (isHidden) return null;
const openMenu = !!menuPosition;
return (
<div ref={ref} className={`relative inline-flex items-center ${className || ''}`} {...props}>
<Tooltip placement='top' title={t('grid.row.add')}>
<IconButton onClick={handleInsertRecordBelow}>
<AddSvg />
</IconButton>
</Tooltip>
<Tooltip placement='top' title={t('grid.row.dragAndClick')}>
<IconButton
onClick={handleOpenMenu}
className='mx-1 cursor-grab active:cursor-grabbing'
draggable={draggable}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
<>
{!isHidden && (
<div ref={ref} className={`relative inline-flex items-center ${className || ''}`} {...props}>
<Tooltip placement='top' title={t('grid.row.add')}>
<IconButton onClick={handleInsertRecordBelow}>
<AddSvg />
</IconButton>
</Tooltip>
<Tooltip placement='top' title={t('grid.row.dragAndClick')}>
<IconButton
onClick={handleOpenMenu}
className='mx-1 cursor-grab active:cursor-grabbing'
draggable={draggable}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<DragSvg className='-mx-1' />
</IconButton>
</Tooltip>
</div>
)}
{openMenu && (
<Popover
open={openMenu}
onClose={handleCloseMenu}
transformOrigin={{
vertical: 'center',
horizontal: 'left',
}}
anchorReference={'anchorPosition'}
anchorPosition={menuPosition}
>
<DragSvg className='-mx-1' />
</IconButton>
</Tooltip>
<Popover
open={openMenu}
onClose={handleCloseMenu}
transformOrigin={{
vertical: 'center',
horizontal: 'left',
}}
anchorOrigin={{
vertical: 'center',
horizontal: 'right',
}}
container={ref.current}
anchorEl={ref.current}
>
<GridCellRowMenu onClickItem={() => handleCloseMenu} rowId={rowId} getPrevRowId={getPrevRowId} />
</Popover>
</div>
<GridCellRowMenu onClickItem={handleCloseMenu} rowId={rowId} getPrevRowId={getPrevRowId} />
</Popover>
)}
</>
);
};

View File

@ -1,41 +1,25 @@
import { Button } from '@mui/material';
import { FieldType } from '@/services/backend';
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
import { fieldService } from '../../application';
import { useDatabaseVisibilityFields } from '../../Database.hooks';
import { GridField } from '../GridField';
import { useViewId } from '@/appflowy_app/hooks';
import { useTranslation } from 'react-i18next';
import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow/constants';
import React from 'react';
import NewProperty from '$app/components/database/components/edit_record/record_properties/NewProperty';
export const GridFieldRow = () => {
const { t } = useTranslation();
const viewId = useViewId();
const fields = useDatabaseVisibilityFields();
const handleClick = async () => {
await fieldService.createField(viewId, FieldType.RichText);
};
return (
<div className='z-10 flex border-b border-line-divider'>
<div className={'flex'}>
{fields.map((field) => {
return <GridField key={field.id} field={field} />;
})}
</div>
<>
<div className='z-10 flex border-b border-line-divider'>
<div className={'flex'}>
{fields.map((field) => {
return <GridField key={field.id} field={field} />;
})}
</div>
<div className={`w-[${DEFAULT_FIELD_WIDTH}px]`}>
<Button
color={'inherit'}
className='flex h-full w-full items-center justify-start whitespace-nowrap text-left'
size='small'
startIcon={<AddSvg />}
onClick={handleClick}
>
{t('grid.field.newColumn')}
</Button>
<div className={`w-[${DEFAULT_FIELD_WIDTH}px]`}>
<NewProperty />
</div>
</div>
</div>
</>
);
};

View File

@ -1,5 +1,5 @@
import { Virtualizer } from '@tanstack/react-virtual';
import { FC } from 'react';
import React, { FC } from 'react';
import { RenderRow, RenderRowType } from './constants';
import { GridCellRow } from './GridCellRow';
import { GridNewRow } from './GridNewRow';
@ -12,7 +12,7 @@ export interface GridRowProps {
getPrevRowId: (id: string) => string | null;
}
export const GridRow: FC<GridRowProps> = ({ row, virtualizer, getPrevRowId }) => {
export const GridRow: FC<GridRowProps> = React.memo(({ row, virtualizer, getPrevRowId }) => {
switch (row.type) {
case RenderRowType.Fields:
return <GridFieldRow />;
@ -25,4 +25,4 @@ export const GridRow: FC<GridRowProps> = ({ row, virtualizer, getPrevRowId }) =>
default:
return null;
}
};
});

View File

@ -1,5 +1,5 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import { FC, useMemo, useRef } from 'react';
import React, { FC, useMemo, useRef } from 'react';
import { RowMeta } from '../../application';
import { useDatabase, useDatabaseVisibilityFields } from '../../Database.hooks';
import { VirtualizedList } from '../../_shared';
@ -13,7 +13,7 @@ const getRenderRowKey = (row: RenderRow) => {
return row.type;
};
export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => {
export const GridTable: FC<{ tableHeight: number }> = React.memo(({ tableHeight }) => {
const verticalScrollElementRef = useRef<HTMLDivElement | null>(null);
const horizontalScrollElementRef = useRef<HTMLDivElement | null>(null);
const { rowMetas } = useDatabase();
@ -21,7 +21,7 @@ export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => {
const fields = useDatabaseVisibilityFields();
const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({
count: renderRows.length,
overscan: 20,
overscan: 5,
getItemKey: (i) => getRenderRowKey(renderRows[i]),
getScrollElement: () => verticalScrollElementRef.current,
estimateSize: () => 37,
@ -73,4 +73,4 @@ export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => {
</div>
</div>
);
};
});

View File

@ -168,7 +168,7 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
[container.scrollLeft, container.scrollTop, dispatch, docId, handleDragEnd]
);
const handleDraging = useCallback(
const handleDragging = useCallback(
(e: MouseEvent) => {
if (!isDragging || !anchorRef.current) return;
@ -208,19 +208,19 @@ export function useBlockRangeSelection(container: HTMLDivElement) {
);
useEffect(() => {
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleDraging);
container.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleDragging);
document.addEventListener('mouseup', handleDragEnd);
container.addEventListener('keydown', onKeyDown, true);
return () => {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mousemove', handleDraging);
container.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mousemove', handleDragging);
document.removeEventListener('mouseup', handleDragEnd);
container.removeEventListener('keydown', onKeyDown, true);
};
}, [handleMouseDown, handleDragEnd, handleDraging, container, onKeyDown]);
}, [handleMouseDown, handleDragEnd, handleDragging, container, onKeyDown]);
return null;
}

View File

@ -53,6 +53,7 @@ export function useBlockRectSelection({ container, getIntersectedBlockIds }: Blo
const handleDragStart = useCallback(
(e: MouseEvent) => {
if (e.button !== 0) return;
if (isPointInBlock(e.target as HTMLElement)) {
return;
}

View File

@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import ChangeCoverButton from '$app/components/document/DocumentBanner/cover/ChangeCoverButton';
import { readImage } from '$app/utils/document/image';
import { CoverType } from '$app/interfaces/document';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
function DocumentCover({
cover,
@ -14,6 +15,7 @@ function DocumentCover({
className?: string;
onUpdateCover: (coverType: CoverType | null, cover: string | null) => void;
}) {
const { docId } = useSubscribeDocument();
const [hover, setHover] = useState(false);
const [leftOffset, setLeftOffset] = useState(0);
const [width, setWidth] = useState(0);
@ -47,13 +49,13 @@ function DocumentCover({
useEffect(() => {
const observer = new ResizeObserver(handleWidthChange);
const docPage = document.getElementById('appflowy-block-doc') as HTMLElement;
const docPage = document.getElementById(`appflowy-block-doc-${docId}`) as HTMLElement;
observer.observe(docPage);
return () => {
observer.disconnect();
};
}, [handleWidthChange]);
}, [handleWidthChange, docId]);
useEffect(() => {
if (coverType === CoverType.Image && cover) {

View File

@ -3,13 +3,16 @@ import { useDocumentTitle } from './DocumentTitle.hooks';
import TextBlock from '../TextBlock';
import { useTranslation } from 'react-i18next';
import DocumentBanner from '$app/components/document/DocumentBanner';
import { ContainerType, useContainerType } from '$app/hooks/document.hooks';
export default function DocumentTitle({ id }: { id: string }) {
const { node } = useDocumentTitle(id);
const { t } = useTranslation();
const [hover, setHover] = useState(false);
if (!node) return null;
const containerType = useContainerType();
if (!node || containerType !== ContainerType.DocumentPage) return null;
return (
<div className={'flex flex-col'} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>

View File

@ -6,10 +6,17 @@ import { withErrorBoundary } from 'react-error-boundary';
import { ErrorBoundaryFallbackComponent } from '../_shared/ErrorBoundaryFallbackComponent';
import VirtualizedList from '../VirtualizedList';
import { Skeleton } from '@mui/material';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
function Root({ documentData }: { documentData: DocumentData }) {
function Root({
documentData,
getDocumentTitle,
}: {
documentData: DocumentData;
getDocumentTitle?: () => React.ReactNode;
}) {
const { node, childIds } = useRoot({ documentData });
const { docId } = useSubscribeDocument();
const renderNode = useCallback((nodeId: string) => {
return <Node key={nodeId} id={nodeId} />;
}, []);
@ -20,8 +27,11 @@ function Root({ documentData }: { documentData: DocumentData }) {
return (
<>
<div id='appflowy-block-doc' className='h-[100%] overflow-hidden text-base text-text-title caret-text-title'>
<VirtualizedList node={node} childIds={childIds} renderNode={renderNode} />
<div
id={`appflowy-block-doc-${docId}`}
className='h-[100%] overflow-hidden text-base text-text-title caret-text-title'
>
<VirtualizedList getDocumentTitle={getDocumentTitle} node={node} childIds={childIds} renderNode={renderNode} />
</div>
</>
);

View File

@ -4,19 +4,34 @@ import Overlay from '../Overlay';
import { Node } from '$app/interfaces/document';
import { useSubscribeDocument } from '$app/components/document/_shared/SubscribeDoc.hooks';
import { ContainerType, useContainerType } from '$app/hooks/document.hooks';
import { useCallback } from 'react';
export default function VirtualizedList({
childIds,
node,
renderNode,
getDocumentTitle,
}: {
childIds: string[];
node: Node;
renderNode: (nodeId: string) => JSX.Element;
getDocumentTitle?: () => React.ReactNode;
}) {
const { virtualize, parentRef } = useVirtualizedList(childIds.length + 1);
const virtualItems = virtualize.getVirtualItems();
const { docId } = useSubscribeDocument();
const containerType = useContainerType();
const isDocumentPage = containerType === ContainerType.DocumentPage;
const renderDocumentTitle = useCallback(() => {
if (getDocumentTitle) {
return getDocumentTitle();
}
return <DocumentTitle id={node.id} />;
}, [getDocumentTitle, node.id]);
return (
<>
@ -26,7 +41,7 @@ export default function VirtualizedList({
className={`doc-scroller-container flex h-[100%] flex-wrap justify-center overflow-auto px-20`}
>
<div
className='doc-body max-w-screen w-[900px] min-w-0'
className={`doc-body ${isDocumentPage ? 'max-w-screen w-[900px] min-w-0' : 'w-full'}`}
style={{
height: virtualize.getTotalSize(),
position: 'relative',
@ -48,10 +63,13 @@ export default function VirtualizedList({
const id = isDocumentTitle ? node.id : childIds[virtualRow.index - 1];
return (
<div className={isDocumentTitle ? '' : 'pt-[0.5px]'} key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
{
isDocumentTitle ? <DocumentTitle id={node.id} /> : renderNode(id)
}
<div
className={isDocumentTitle ? '' : 'pt-[0.5px]'}
key={id}
data-index={virtualRow.index}
ref={virtualize.measureElement}
>
{isDocumentTitle ? renderDocumentTitle() : renderNode(id)}
</div>
);
})}

View File

@ -0,0 +1,24 @@
import React from 'react';
import { ContainerType, ContainerTypeProvider, useDocument } from '$app/hooks/document.hooks';
import { DocumentControllerContext } from '$app/stores/effects/document/document_controller';
import Root from '$app/components/document/Root';
interface Props {
documentId: string;
containerType?: ContainerType;
getDocumentTitle?: () => React.ReactNode;
}
function Document({ documentId, getDocumentTitle, containerType = ContainerType.DocumentPage }: Props) {
const { documentData, controller } = useDocument(documentId);
if (!documentId || !documentData || !controller) return null;
return (
<ContainerTypeProvider value={containerType}>
<DocumentControllerContext.Provider value={controller}>
<Root getDocumentTitle={getDocumentTitle} documentData={documentData} />
</DocumentControllerContext.Provider>
</ContainerTypeProvider>
);
}
export default Document;

View File

@ -48,7 +48,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void })
onMouseDown={(e) => e.stopPropagation()}
open={open}
TransitionComponent={SlideTransition}
keepMounted
keepMounted={false}
onClose={onClose}
>
<DialogTitle>{t('settings.title')}</DialogTitle>

View File

@ -0,0 +1,102 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { DocumentData } from '../interfaces/document';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { useAppDispatch } from '../stores/store';
import { Log } from '../utils/log';
import {
documentActions,
rangeActions,
rectSelectionActions,
slashCommandActions,
} from '$app/stores/reducers/document/slice';
import { BlockEventPayloadPB } from '@/services/backend/models/flowy-document2';
export const useDocument = (documentId?: string) => {
const [documentData, setDocumentData] = useState<DocumentData>();
const [controller, setController] = useState<DocumentController | null>(null);
const dispatch = useAppDispatch();
const onDocumentChange = useCallback(
(props: { docId: string; isRemote: boolean; data: BlockEventPayloadPB }) => {
dispatch(documentActions.onDataChange(props));
},
[dispatch]
);
const initializeDocument = useCallback(
(docId: string) => {
Log.debug('initialize document', docId);
dispatch(documentActions.initialState(docId));
dispatch(rangeActions.initialState(docId));
dispatch(rectSelectionActions.initialState(docId));
dispatch(slashCommandActions.initialState(docId));
},
[dispatch]
);
const clearDocument = useCallback(
(docId: string) => {
Log.debug('clear document', docId);
dispatch(documentActions.clear(docId));
dispatch(rangeActions.clear(docId));
dispatch(rectSelectionActions.clear(docId));
dispatch(slashCommandActions.clear(docId));
},
[dispatch]
);
useEffect(() => {
let documentController: DocumentController | null = null;
void (async () => {
if (!documentId) return;
documentController = new DocumentController(documentId, onDocumentChange);
const docId = documentController.documentId;
Log.debug('open document', documentId);
initializeDocument(documentController.documentId);
setController(documentController);
try {
const res = await documentController.open();
if (!res) return;
dispatch(
documentActions.create({
...res,
docId,
})
);
setDocumentData(res);
} catch (e) {
Log.error(e);
}
})();
return () => {
if (documentController) {
void (async () => {
await documentController.dispose();
clearDocument(documentController.documentId);
})();
}
Log.debug('close document', documentId);
};
}, [clearDocument, dispatch, initializeDocument, onDocumentChange, documentId]);
return { documentId, documentData, controller };
};
export enum ContainerType {
DocumentPage,
EditRecord,
}
export const ContainerTypeContext = createContext(ContainerType.DocumentPage);
export const ContainerTypeProvider = ContainerTypeContext.Provider;
export const useContainerType = () => {
return useContext(ContainerTypeContext);
};

View File

@ -10,6 +10,6 @@ void i18next
.init({
lng: 'en',
defaultNS: 'translation',
debug: true,
debug: false,
fallbackLng: 'en',
});

View File

@ -17,6 +17,8 @@ import {
ViewIconPB,
UpdateViewIconPayloadPB,
FolderEventUpdateViewIcon,
FolderEventCreateOrphanView,
CreateOrphanViewPayloadPB,
} from '@/services/backend/events/flowy-folder2';
import { Page, PageIcon } from '$app_reducers/pages/slice';
@ -88,6 +90,12 @@ export class PageBackendService {
return FolderEventDuplicateView(payload);
};
createOrphanPage = async (params: ReturnType<typeof CreateOrphanViewPayloadPB.prototype.toObject>) => {
const payload = CreateOrphanViewPayloadPB.fromObject(params);
return FolderEventCreateOrphanView(payload);
};
closePage = async (viewId: string) => {
const payload = new ViewIdPB({
value: viewId,

View File

@ -53,14 +53,14 @@ export class PageController {
return [];
};
getPage = async (id?: string): Promise<Page> => {
getPage = async (id?: string) => {
const result = await this.backendService.getPage(id || this.id);
if (result.ok) {
return parserViewPBToPage(result.val);
}
return Promise.reject(result.err);
return Promise.reject(result.val);
};
getParentPage = async (): Promise<Page> => {
@ -128,4 +128,18 @@ export class PageController {
return Promise.reject(result.err);
};
createOrphanPage = async (params: { name: string; layout: ViewLayoutPB }): Promise<Page> => {
const result = await this.backendService.createOrphanPage({
view_id: this.id,
name: params.name,
layout: params.layout,
});
if (result.ok) {
return parserViewPBToPage(result.val);
}
return Promise.reject(result.val);
};
}

View File

@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { DocumentData } from '../interfaces/document';
import { DocumentController } from '$app/stores/effects/document/document_controller';
import { useAppDispatch } from '../stores/store';
@ -12,9 +11,7 @@ import {
} from '$app/stores/reducers/document/slice';
import { BlockEventPayloadPB } from '@/services/backend/models/flowy-document2';
export const useDocument = () => {
const params = useParams();
const [documentId, setDocumentId] = useState<string>();
export const useDocument = (documentId?: string) => {
const [documentData, setDocumentData] = useState<DocumentData>();
const [controller, setController] = useState<DocumentController | null>(null);
const dispatch = useAppDispatch();
@ -52,11 +49,11 @@ export const useDocument = () => {
let documentController: DocumentController | null = null;
void (async () => {
if (!params?.id) return;
documentController = new DocumentController(params.id, onDocumentChange);
if (!documentId) return;
documentController = new DocumentController(documentId, onDocumentChange);
const docId = documentController.documentId;
Log.debug('open document', params.id);
Log.debug('open document', documentId);
initializeDocument(documentController.documentId);
@ -72,7 +69,6 @@ export const useDocument = () => {
})
);
setDocumentData(res);
setDocumentId(params.id);
} catch (e) {
Log.error(e);
}
@ -86,9 +82,9 @@ export const useDocument = () => {
})();
}
Log.debug('close document', params.id);
Log.debug('close document', documentId);
};
}, [clearDocument, dispatch, initializeDocument, onDocumentChange, params.id]);
}, [clearDocument, dispatch, initializeDocument, onDocumentChange, documentId]);
return { documentId, documentData, controller };
};

View File

@ -1,14 +1,12 @@
import { useDocument } from './DocumentPage.hooks';
import Root from '../components/document/Root';
import { DocumentControllerContext } from '../stores/effects/document/document_controller';
import { useParams } from 'react-router-dom';
import Document from '$app/components/document';
export const DocumentPage = () => {
const { documentId, documentData, controller } = useDocument();
const params = useParams();
if (!documentId || !documentData || !controller) return null;
return (
<DocumentControllerContext.Provider value={controller}>
<Root documentData={documentData} />
</DocumentControllerContext.Provider>
);
const documentId = params.id;
if (!documentId) return null;
return <Document documentId={documentId} />;
};

View File

@ -28,9 +28,6 @@ body {
height: 8px;
}
div[role="textbox"] ::selection {
@apply bg-transparent;
}
:root[data-dark-mode=true] body {
scrollbar-color: #fff var(--bg-body);