mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 15:51:44 +03:00
Merge pull request #325 from toeverything/feat/kanban-editable
Feat/kanban editable
This commit is contained in:
commit
047368130e
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 />
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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]
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
}));
|
||||||
|
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user