mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support modified cell in modal (#3948)
* feat: support drag fields in modal * fix: wrong draggable position
This commit is contained in:
parent
729b8571b5
commit
68de83c611
@ -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 |
@ -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 |
16
frontend/appflowy_tauri/src/appflowy_app/assets/eye_open.svg
Normal file
16
frontend/appflowy_tauri/src/appflowy_app/assets/eye_open.svg
Normal 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 |
6
frontend/appflowy_tauri/src/appflowy_app/assets/open.svg
Normal file
6
frontend/appflowy_tauri/src/appflowy_app/assets/open.svg
Normal 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 |
@ -114,7 +114,7 @@ function BlockDragDropContext({ children }: { children: React.ReactNode }) {
|
||||
left: draggingPosition?.x,
|
||||
pointerEvents: 'none',
|
||||
opacity: dragShadowVisible ? 1 : 0,
|
||||
zIndex: 1000,
|
||||
zIndex: 2000,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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: {
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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);
|
@ -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;
|
@ -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));
|
@ -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>
|
||||
);
|
||||
});
|
@ -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));
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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 (
|
@ -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>
|
||||
);
|
||||
};
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
};
|
@ -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)}</>;
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
export * from './Field';
|
||||
export * from './FieldSelect';
|
||||
export * from './FieldsMenu';
|
||||
export * from './FieldListMenu';
|
||||
export * from './FieldTypeText';
|
||||
export * from './FieldTypeSvg';
|
||||
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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} />;
|
||||
};
|
@ -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]}`);
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 && (
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)}>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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;
|
@ -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>
|
||||
|
102
frontend/appflowy_tauri/src/appflowy_app/hooks/document.hooks.ts
Normal file
102
frontend/appflowy_tauri/src/appflowy_app/hooks/document.hooks.ts
Normal 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);
|
||||
};
|
@ -10,6 +10,6 @@ void i18next
|
||||
.init({
|
||||
lng: 'en',
|
||||
defaultNS: 'translation',
|
||||
debug: true,
|
||||
debug: false,
|
||||
fallbackLng: 'en',
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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} />;
|
||||
};
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user