1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-07 18:03:48 +00:00

kanban supports collapse (#7437)

* kanban supports collapse

* optimize

* optimize ui

---------

Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
Aries
2025-02-08 17:56:36 +08:00
committed by GitHub
parent 65ef9f704b
commit cb3af6e66a
5 changed files with 108 additions and 59 deletions

View File

@@ -35,7 +35,7 @@
} }
.file-details-collapse .file-details-collapse-header .file-details-collapse-header-operation:hover { .file-details-collapse .file-details-collapse-header .file-details-collapse-header-operation:hover {
background-color: #DBDBDB; background-color: #EFEFEF;
cursor: pointer; cursor: pointer;
border-radius: 3px; border-radius: 3px;
} }

View File

@@ -27,3 +27,25 @@
color: #666666; color: #666666;
margin-left: 12px; margin-left: 12px;
} }
.sf-metadata-view-kanban-board-header .board-header-operation-btn {
display: flex;
align-items: flex-end;
}
.board-header-operation-btn .kanban-header-op-btn {
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 0.5rem;
color: #666666;
cursor: pointer;
border-radius: 3px;
}
.board-header-operation-btn .kanban-header-op-btn.kanban-header-collapse-btn:hover,
.board-header-operation-btn .kanban-header-op-btn.kanban-more-operations-toggle:hover {
background-color: #EFEFEF;
}

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import CellFormatter from '../../../../../components/cell-formatter'; import CellFormatter from '../../../../../components/cell-formatter';
import { gettext } from '../../../../../../utils/constants'; import { gettext } from '../../../../../../utils/constants';
import OpMenu from './op-menu'; import OpMenu from './op-menu';
@@ -7,9 +8,11 @@ import { CellType } from '../../../../../constants';
import './index.css'; import './index.css';
const Header = ({ readonly, haveFreezed, value, groupByColumn, cardsQuantity, onDelete, onFreezed, onUnFreezed }) => { const Header = ({ readonly, haveFreezed, value, groupByColumn, cardsQuantity, onDelete, onFreezed, onUnFreezed, isCollapsed, onCollapse }) => {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const headerRef = useRef();
const onMouseEnter = useCallback(() => { const onMouseEnter = useCallback(() => {
if (haveFreezed) return; if (haveFreezed) return;
setActive(true); setActive(true);
@@ -20,10 +23,14 @@ const Header = ({ readonly, haveFreezed, value, groupByColumn, cardsQuantity, on
setActive(false); setActive(false);
}, [haveFreezed]); }, [haveFreezed]);
const handelUnFreezed = useCallback((keepActive) => { const keepActive = useCallback((event) => {
return event.target.className?.includes('kanban-header-op-btn') || event.target === headerRef.current;
}, []);
const handelUnFreezed = useCallback((event) => {
onUnFreezed(); onUnFreezed();
!keepActive && setActive(false); !keepActive(event) && setActive(false);
}, [onUnFreezed]); }, [onUnFreezed, keepActive]);
const titleValue = useMemo(() => { const titleValue = useMemo(() => {
if (!value || !groupByColumn) return null; if (!value || !groupByColumn) return null;
@@ -31,9 +38,14 @@ const Header = ({ readonly, haveFreezed, value, groupByColumn, cardsQuantity, on
return value; return value;
}, [value, groupByColumn]); }, [value, groupByColumn]);
const handleCollapse = useCallback((event) => {
onCollapse();
!keepActive(event) && setActive(false);
}, [onCollapse, keepActive]);
return ( return (
<div className="sf-metadata-view-kanban-board-header" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> <div className="sf-metadata-view-kanban-board-header" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<div className="sf-metadata-view-kanban-board-header-title"> <div className="sf-metadata-view-kanban-board-header-title" ref={headerRef}>
{value ? ( {value ? (
<CellFormatter value={titleValue} field={groupByColumn} readonly={true} /> <CellFormatter value={titleValue} field={groupByColumn} readonly={true} />
) : ( ) : (
@@ -41,8 +53,15 @@ const Header = ({ readonly, haveFreezed, value, groupByColumn, cardsQuantity, on
)} )}
<span className="cards-quantity">{cardsQuantity}</span> <span className="cards-quantity">{cardsQuantity}</span>
</div> </div>
{value && !readonly && active && ( {active && (
<OpMenu onDelete={onDelete} onFreezed={onFreezed} onUnFreezed={handelUnFreezed} /> <div className="board-header-operation-btn">
{value && !readonly && <OpMenu onDelete={onDelete} onFreezed={onFreezed} onUnFreezed={handelUnFreezed} />}
<i
className={classNames('sf3-font sf3-font-down kanban-header-op-btn kanban-header-collapse-btn', { 'rotate-90': isCollapsed })}
title={isCollapsed ? gettext('Expand') : gettext('Collapse')}
onClick={handleCollapse}>
</i>
</div>
)} )}
</div> </div>
); );

View File

@@ -9,8 +9,7 @@ const OpMenu = ({ onDelete, onFreezed, onUnFreezed }) => {
const toggle = useCallback((event) => { const toggle = useCallback((event) => {
event.stopPropagation(); event.stopPropagation();
if (isShow) { if (isShow) {
const isClickToggleBtn = event.target.className?.includes('kanban-more-operations-toggle'); onUnFreezed(event);
onUnFreezed(isClickToggleBtn);
} else { } else {
onFreezed(); onFreezed();
} }
@@ -35,7 +34,7 @@ const OpMenu = ({ onDelete, onFreezed, onUnFreezed }) => {
tag="i" tag="i"
role="button" role="button"
tabIndex="0" tabIndex="0"
className="sf-dropdown-toggle sf3-font-more sf3-font kanban-more-operations-toggle" className="sf-dropdown-toggle sf3-font-more sf3-font kanban-header-op-btn kanban-more-operations-toggle"
title={gettext('More operations')} title={gettext('More operations')}
aria-label={gettext('More operations')} aria-label={gettext('More operations')}
data-toggle="dropdown" data-toggle="dropdown"

View File

@@ -31,6 +31,7 @@ const Board = ({
onContextMenu, onContextMenu,
}) => { }) => {
const [isDraggingOver, setDraggingOver] = useState(false); const [isDraggingOver, setDraggingOver] = useState(false);
const [isCollapsed, setCollapsed] = useState(false);
const boardName = useMemo(() => `sf_metadata_kanban_board_${board.key}`, [board]); const boardName = useMemo(() => `sf_metadata_kanban_board_${board.key}`, [board]);
const cardsQuantity = useMemo(() => board.children.length, [board.children]); const cardsQuantity = useMemo(() => board.children.length, [board.children]);
@@ -54,6 +55,10 @@ const Board = ({
setTimeout(() => updateDragging(false), 0); setTimeout(() => updateDragging(false), 0);
}, [isDraggingOver, onMove, updateDragging]); }, [isDraggingOver, onMove, updateDragging]);
const onCollapse = useCallback(() => {
setCollapsed(!isCollapsed);
}, [isCollapsed]);
return ( return (
<section draggable={false} className="sf-metadata-view-kanban-board"> <section draggable={false} className="sf-metadata-view-kanban-board">
<Header <Header
@@ -65,55 +70,59 @@ const Board = ({
onDelete={() => deleteOption(board.key)} onDelete={() => deleteOption(board.key)}
onFreezed={onFreezed} onFreezed={onFreezed}
onUnFreezed={onUnFreezed} onUnFreezed={onUnFreezed}
isCollapsed={isCollapsed}
onCollapse={onCollapse}
/> />
<Container {!isCollapsed && (
orientation="vertical" <Container
groupName={boardName} orientation="vertical"
dragClass="kanban-drag-card" groupName={boardName}
dropClass="kanban-drop-card" dragClass="kanban-drag-card"
onDragStart={onDragStart} dropClass="kanban-drop-card"
onDrop={e => onDragEnd(boardIndex, e)} onDragStart={onDragStart}
onDragEnter={() => setDraggingOver(true)} onDrop={e => onDragEnd(boardIndex, e)}
onDragLeave={() => setDraggingOver(false)} onDragEnter={() => setDraggingOver(true)}
shouldAcceptDrop={(sourceContainer) => sourceContainer.groupName !== boardName} onDragLeave={() => setDraggingOver(false)}
getChildPayload={(cardIndex) => ({ boardIndex, cardIndex })} shouldAcceptDrop={(sourceContainer) => sourceContainer.groupName !== boardName}
dropPlaceholder={{ getChildPayload={(cardIndex) => ({ boardIndex, cardIndex })}
animationDuration: 300, dropPlaceholder={{
showOnTop: true, animationDuration: 300,
className: 'card-drop-preview', showOnTop: true,
}} className: 'card-drop-preview',
getGhostParent={() => { }}
getGhostParent={() => {
// return anchestor of container who doesn't have a transform property // return anchestor of container who doesn't have a transform property
return document.querySelector('.sf-metadata-main'); return document.querySelector('.sf-metadata-main');
}} }}
> >
{board.children.map((cardKey) => { {board.children.map((cardKey) => {
const record = getRowById(metadata, cardKey); const record = getRowById(metadata, cardKey);
if (!record) return null; if (!record) return null;
const recordId = getRecordIdFromRecord(record); const recordId = getRecordIdFromRecord(record);
const isSelected = selectedCard === recordId; const isSelected = selectedCard === recordId;
const CardElement = ( const CardElement = (
<Card <Card
key={cardKey} key={cardKey}
isSelected={isSelected} isSelected={isSelected}
displayEmptyValue={displayEmptyValue} displayEmptyValue={displayEmptyValue}
displayColumnName={displayColumnName} displayColumnName={displayColumnName}
record={record} record={record}
titleColumn={titleColumn} titleColumn={titleColumn}
displayColumns={displayColumns} displayColumns={displayColumns}
onOpenFile={onOpenFile} onOpenFile={onOpenFile}
onSelectCard={onSelectCard} onSelectCard={onSelectCard}
onContextMenu={(e) => onContextMenu(e, recordId)} onContextMenu={(e) => onContextMenu(e, recordId)}
/> />
); );
if (readonly) return CardElement; if (readonly) return CardElement;
return ( return (
<Draggable key={`sf-metadata-kanban-card-${cardKey}`}> <Draggable key={`sf-metadata-kanban-card-${cardKey}`}>
{CardElement} {CardElement}
</Draggable> </Draggable>
); );
})} })}
</Container> </Container>
)}
</section> </section>
); );
}; };