Merge pull request #325 from toeverything/feat/kanban-editable

Feat/kanban editable
This commit is contained in:
mitsuha(XiWen TU) 2022-08-25 16:43:25 +08:00 committed by GitHub
commit 047368130e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 165 additions and 68 deletions

View File

@ -1,12 +1,12 @@
import { useCallback } from 'react';
import { CardItem } from './CardItem';
import { styled } from '@toeverything/components/ui';
import { useKanban } from '@toeverything/components/editor-core';
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
import type { import type {
KanbanCard, KanbanCard,
KanbanGroup, KanbanGroup,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
import { useKanban } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui';
import { useCallback } from 'react';
import { CardItem } from './CardItem';
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
const AddCardWrapper = styled('div')({ const AddCardWrapper = styled('div')({
display: 'flex', display: 'flex',
@ -48,7 +48,7 @@ export const CardContext = (props: Props) => {
item={item} item={item}
active={activeId === id} active={activeId === id}
> >
<CardItem id={id} block={block} /> <CardItem block={block} />
</CardItemPanelWrapper> </CardItemPanelWrapper>
</StyledCardContainer> </StyledCardContainer>
); );

View File

@ -1,6 +1,6 @@
import { import {
KanbanBlockRender,
KanbanCard, KanbanCard,
useBlockRender,
useEditor, useEditor,
useKanban, useKanban,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
@ -10,7 +10,6 @@ import {
MuiClickAwayListener, MuiClickAwayListener,
styled, styled,
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
import { useFlag } from '@toeverything/datasource/feature-flags';
import { useState, type MouseEvent } from 'react'; import { useState, type MouseEvent } from 'react';
import { useRefPage } from './RefPage'; import { useRefPage } from './RefPage';
@ -82,42 +81,37 @@ const Overlay = styled('div')({
}, },
}); });
export const CardItem = ({ export const CardItem = ({ block }: { block: KanbanCard['block'] }) => {
id,
block,
}: {
id: KanbanCard['id'];
block: KanbanCard['block'];
}) => {
const { addSubItem } = useKanban(); const { addSubItem } = useKanban();
const { openSubPage } = useRefPage(); const { openSubPage } = useRefPage();
const [editable, setEditable] = useState(false); const [editableBlock, setEditableBlock] = useState<string | null>(null);
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
const { editor } = useEditor(); const { editor } = useEditor();
const { BlockRender } = useBlockRender();
const onAddItem = async () => { const onAddItem = async () => {
setEditable(true); const newItem = await addSubItem(block);
await addSubItem(block); setEditableBlock(newItem.id);
}; };
const onClickCard = async () => { const onClickCard = async () => {
openSubPage(id); openSubPage(block.id);
}; };
const onClickPen = (e: MouseEvent<Element>) => { const onClickPen = (e: MouseEvent<Element>) => {
e.stopPropagation(); e.stopPropagation();
setEditable(true); setEditableBlock(block.id);
editor.selectionManager.activeNodeByNodeId(block.id); editor.selectionManager.activeNodeByNodeId(block.id);
}; };
return ( return (
<MuiClickAwayListener onClickAway={() => setEditable(false)}> <MuiClickAwayListener onClickAway={() => setEditableBlock(null)}>
<CardContainer> <CardContainer>
<CardContent> <CardContent>
<BlockRender blockId={id} /> <KanbanBlockRender
blockId={block.id}
activeBlock={editableBlock}
/>
</CardContent> </CardContent>
{showKanbanRefPageFlag && !editable && ( {!editableBlock && (
<Overlay onClick={onClickCard}> <Overlay onClick={onClickCard}>
<IconButton backgroundColor="#fff" onClick={onClickPen}> <IconButton backgroundColor="#fff" onClick={onClickPen}>
<PenIcon /> <PenIcon />

View File

@ -1,7 +1,6 @@
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
import { CardItem } from '../../CardItem'; import { CardItem } from '../../CardItem';
import type { KanbanCard } from '@toeverything/components/editor-core';
import type { DndableItems } from '../type'; import type { DndableItems } from '../type';
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
export function renderContainerDragOverlay({ export function renderContainerDragOverlay({
containerId, containerId,
@ -18,7 +17,7 @@ export function renderContainerDragOverlay({
return ( return (
<CardItemWrapper <CardItemWrapper
key={id} key={id}
card={<CardItem key={id} id={id} block={block} />} card={<CardItem key={id} block={block} />}
index={index} index={index}
/> />
); );

View File

@ -1,10 +1,10 @@
import type { PropsWithChildren } from 'react';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
import type { PropsWithChildren } from 'react';
import { useRef } from 'react';
import type { AsyncBlock } from '../editor'; import type { AsyncBlock } from '../editor';
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
import { PendantPopover } from './pendant-popover'; import { PendantPopover } from './pendant-popover';
import { PendantRender } from './pendant-render'; import { PendantRender } from './pendant-render';
import { useRef } from 'react';
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
/** /**
* @deprecated * @deprecated
*/ */

View File

@ -1,15 +1,15 @@
import { ComponentType, ReactElement } from 'react';
import type { Column } from '@toeverything/datasource/db-service'; import type { Column } from '@toeverything/datasource/db-service';
import { import {
ArrayOperation, ArrayOperation,
BlockDecoration, BlockDecoration,
MapOperation, MapOperation,
} from '@toeverything/datasource/jwt'; } from '@toeverything/datasource/jwt';
import type { ComponentType, ReactElement } from 'react';
import type { EventData } from '../block'; import type { EventData } from '../block';
import { AsyncBlock } from '../block'; import { AsyncBlock } from '../block';
import { HTML2BlockResult } from '../clipboard';
import type { Editor } from '../editor'; import type { Editor } from '../editor';
import { SelectBlock } from '../selection'; import { SelectBlock } from '../selection';
import { HTML2BlockResult } from '../clipboard';
export interface CreateView { export interface CreateView {
block: AsyncBlock; block: AsyncBlock;
editor: Editor; editor: Editor;

View File

@ -6,11 +6,6 @@ export * from './kanban';
export * from './kanban/types'; export * from './kanban/types';
export * from './recast-block'; export * from './recast-block';
export * from './recast-block/types'; export * from './recast-block/types';
export { export * from './render-block';
BlockRenderProvider,
RenderBlockChildren,
useBlockRender,
withTreeViewChildren,
} from './render-block';
export { MIN_PAGE_WIDTH, RenderRoot } from './RenderRoot'; export { MIN_PAGE_WIDTH, RenderRoot } from './RenderRoot';
export * from './utils'; export * from './utils';

View File

@ -10,8 +10,6 @@ import {
RecastMetaProperty, RecastMetaProperty,
RecastPropertyId, RecastPropertyId,
} from '../recast-block/types'; } from '../recast-block/types';
import { BlockRenderProvider } from '../render-block';
import { KanbanBlockRender } from '../render-block/RenderKanbanBlock';
import { useInitKanbanEffect, useRecastKanban } from './kanban'; import { useInitKanbanEffect, useRecastKanban } from './kanban';
import { KanbanGroup } from './types'; import { KanbanGroup } from './types';
@ -56,11 +54,9 @@ export const KanbanProvider = ({
}; };
return ( return (
<BlockRenderProvider blockRender={KanbanBlockRender}> <KanbanContext.Provider value={value}>
<KanbanContext.Provider value={value}> {children}
{children} </KanbanContext.Provider>
</KanbanContext.Provider>
</BlockRenderProvider>
); );
}; };

View File

@ -358,6 +358,7 @@ export const useKanban = () => {
} }
card.append(newBlock); card.append(newBlock);
editor.selectionManager.activeNodeByNodeId(newBlock.id); editor.selectionManager.activeNodeByNodeId(newBlock.id);
return newBlock;
}, },
[editor] [editor]
); );

View File

@ -1,7 +1,7 @@
import { Protocol } from '@toeverything/datasource/db-service'; import { Protocol } from '@toeverything/datasource/db-service';
import type { AsyncBlock, BlockEditor } from '../editor'; import type { AsyncBlock, BlockEditor } from '../editor';
import type { RecastBlock } from '.';
import { cloneRecastMetaTo, mergeRecastMeta } from './property'; import { cloneRecastMetaTo, mergeRecastMeta } from './property';
import type { RecastBlock } from './types';
const mergeGroupProperties = async (...groups: RecastBlock[]) => { const mergeGroupProperties = async (...groups: RecastBlock[]) => {
const [headGroup, ...restGroups] = groups; const [headGroup, ...restGroups] = groups;

View File

@ -2,6 +2,7 @@ import { nanoid } from 'nanoid';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { AsyncBlock } from '../editor'; import { AsyncBlock } from '../editor';
import { useRecastBlock } from './Context'; import { useRecastBlock } from './Context';
import { getHistory, removeHistory, setHistory } from './history';
import type { RecastBlock, RecastItem, StatusProperty } from './types'; import type { RecastBlock, RecastItem, StatusProperty } from './types';
import { import {
META_PROPERTIES_KEY, META_PROPERTIES_KEY,
@ -15,7 +16,6 @@ import {
SelectProperty, SelectProperty,
TABLE_VALUES_KEY, TABLE_VALUES_KEY,
} from './types'; } from './types';
import { getHistory, removeHistory, setHistory } from './history';
/** /**
* Generate a unique id for a property * Generate a unique id for a property
@ -275,7 +275,7 @@ const isSelectLikeProperty = (
metaProperty?: RecastMetaProperty metaProperty?: RecastMetaProperty
): metaProperty is SelectProperty | MultiSelectProperty | StatusProperty => { ): metaProperty is SelectProperty | MultiSelectProperty | StatusProperty => {
return ( return (
metaProperty && !!metaProperty &&
(metaProperty.type === PropertyType.Status || (metaProperty.type === PropertyType.Status ||
metaProperty.type === PropertyType.Select || metaProperty.type === PropertyType.Select ||
metaProperty.type === PropertyType.MultiSelect) metaProperty.type === PropertyType.MultiSelect)

View File

@ -1,4 +1,7 @@
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
import { Protocol } from '@toeverything/datasource/db-service';
import { useEffect, useState } from 'react';
import { AsyncBlock } from '../editor';
import { useBlock } from '../hooks'; import { useBlock } from '../hooks';
import { BlockRenderProvider } from './Context'; import { BlockRenderProvider } from './Context';
import { NullBlockRender, RenderBlock, RenderBlockProps } from './RenderBlock'; import { NullBlockRender, RenderBlock, RenderBlockProps } from './RenderBlock';
@ -25,32 +28,140 @@ const OneLevelBlockRender = ({ blockId }: RenderBlockProps) => {
); );
}; };
export const KanbanBlockRender = ({ blockId }: RenderBlockProps) => { export const KanbanParentBlockRender = ({
const { block } = useBlock(blockId); blockId,
active,
}: RenderBlockProps & { active?: boolean }) => {
return (
<BlockBorder active={active}>
<BlockWithoutChildrenRender blockId={blockId} />
</BlockBorder>
);
};
if (!block) { const useBlockProgress = (block?: AsyncBlock) => {
return ( const [progress, setProgress] = useState(1);
<BlockRenderProvider blockRender={NullBlockRender}>
<RenderBlock blockId={blockId} /> useEffect(() => {
</BlockRenderProvider> if (!block) {
); return;
}
const updateProgress = async () => {
const children = await block.children();
const todoChildren = children.filter(
child => child.type === Protocol.Block.Type.todo
);
const checkedTodoChildren = todoChildren.filter(
child => child.getProperty('checked')?.value === true
);
setProgress(checkedTodoChildren.length / todoChildren.length);
};
let childrenQueue: (() => void)[] = [];
const childrenUnobserve = () => {
childrenQueue.forEach(fn => fn());
childrenQueue = [];
};
const observeChildren = async () => {
const children = await block.children();
childrenUnobserve();
children.forEach(child => {
const unobserve = child.onUpdate(() => {
updateProgress();
});
childrenQueue.push(unobserve);
});
};
observeChildren();
updateProgress();
const unobserve = block.onUpdate(() => {
observeChildren();
updateProgress();
});
return () => {
unobserve();
childrenUnobserve();
};
}, [block]);
return progress;
};
const KanbanChildrenRender = ({
blockId,
activeBlock,
}: RenderBlockProps & { activeBlock?: string | null }) => {
const { block } = useBlock(blockId);
const progress = useBlockProgress(block);
if (!block || !block?.childrenIds.length) {
return null;
} }
return ( return (
<BlockRenderProvider blockRender={NullBlockRender}> <BlockRenderProvider blockRender={NullBlockRender}>
<RenderBlock blockId={blockId} /> <ProgressBar progress={progress} />
{block?.childrenIds.map(childId => ( {block?.childrenIds.map(childId => (
<StyledBorder key={childId}> <ChildBorder key={childId} active={activeBlock === childId}>
<RenderBlock blockId={childId} /> <RenderBlock blockId={childId} />
</StyledBorder> </ChildBorder>
))} ))}
</BlockRenderProvider> </BlockRenderProvider>
); );
}; };
const StyledBorder = styled('div')({ export const KanbanBlockRender = ({
border: '1px solid #E0E6EB', blockId,
borderRadius: '5px', activeBlock,
margin: '4px', }: RenderBlockProps & { activeBlock?: string | null }) => {
padding: '0 4px', return (
}); <BlockRenderProvider blockRender={NullBlockRender}>
<KanbanParentBlockRender
blockId={blockId}
active={activeBlock === blockId}
/>
<KanbanChildrenRender blockId={blockId} activeBlock={activeBlock} />
</BlockRenderProvider>
);
};
const BlockBorder = styled('div')<{ active?: boolean }>(
({ theme, active }) => ({
borderRadius: '5px',
padding: '0 4px',
border: `1px solid ${
active ? theme.affine.palette.primary : 'transparent'
}`,
})
);
const ProgressBar = styled('div')<{ progress?: number }>(
({ progress = 1 }) => ({
height: '3px',
width: '100%',
background: '#CFE5FF',
borderRadius: '5px',
overflow: 'hidden',
margin: '12px 0',
'::after': {
content: '""',
position: 'relative',
display: 'flex',
background: '#60A5FA',
height: '100%',
width: `${(progress * 100).toFixed(2)}%`,
transition: 'ease 0.5s all',
},
})
);
const ChildBorder = styled(BlockBorder)(({ active, theme }) => ({
border: `1px solid ${active ? theme.affine.palette.primary : '#E0E6EB'}`,
margin: '4px 0',
}));

View File

@ -1,4 +1,5 @@
export { BlockRenderProvider, useBlockRender } from './Context'; export { BlockRenderProvider, useBlockRender } from './Context';
export * from './RenderBlock'; export { NullBlockRender, RenderBlock } from './RenderBlock';
export * from './RenderBlockChildren'; export { RenderBlockChildren } from './RenderBlockChildren';
export { KanbanBlockRender } from './RenderKanbanBlock';
export { withTreeViewChildren } from './WithTreeViewChildren'; export { withTreeViewChildren } from './WithTreeViewChildren';