Manual card order
This commit is contained in:
parent
f5667304f6
commit
8794145802
9 changed files with 163 additions and 30 deletions
|
@ -16,6 +16,7 @@ interface BoardView extends IBlock {
|
|||
readonly visibleOptionIds: readonly string[]
|
||||
readonly hiddenOptionIds: readonly string[]
|
||||
readonly filter: FilterGroup | undefined
|
||||
readonly cardOrder: readonly string[]
|
||||
}
|
||||
|
||||
class MutableBoardView extends MutableBlock {
|
||||
|
@ -68,6 +69,13 @@ class MutableBoardView extends MutableBlock {
|
|||
this.fields.filter = value
|
||||
}
|
||||
|
||||
get cardOrder(): string[] {
|
||||
return this.fields.cardOrder
|
||||
}
|
||||
set cardOrder(value: string[]) {
|
||||
this.fields.cardOrder = value
|
||||
}
|
||||
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
|
||||
|
@ -78,6 +86,7 @@ class MutableBoardView extends MutableBlock {
|
|||
this.visibleOptionIds = block.fields?.visibleOptionIds?.slice() || []
|
||||
this.hiddenOptionIds = block.fields?.hiddenOptionIds?.slice() || []
|
||||
this.filter = new FilterGroup(block.fields?.filter)
|
||||
this.cardOrder = block.fields?.cardOrder?.slice() || []
|
||||
|
||||
if (!this.viewType) {
|
||||
this.viewType = 'board'
|
||||
|
|
|
@ -20,14 +20,17 @@ type BoardCardProps = {
|
|||
card: Card
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
isSelected: boolean
|
||||
isDropZone?: boolean
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
onDragStart?: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
onDragEnd?: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
onDrop?: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
intl: IntlShape
|
||||
}
|
||||
|
||||
type BoardCardState = {
|
||||
isDragged?: boolean
|
||||
isDragOver?: boolean
|
||||
}
|
||||
|
||||
class BoardCard extends React.Component<BoardCardProps, BoardCardState> {
|
||||
|
@ -43,7 +46,10 @@ class BoardCard extends React.Component<BoardCardProps, BoardCardState> {
|
|||
render(): JSX.Element {
|
||||
const {card, intl} = this.props
|
||||
const visiblePropertyTemplates = this.props.visiblePropertyTemplates || []
|
||||
const className = this.props.isSelected ? 'BoardCard selected' : 'BoardCard'
|
||||
let className = this.props.isSelected ? 'BoardCard selected' : 'BoardCard'
|
||||
if (this.props.isDropZone && this.state.isDragOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
const element = (
|
||||
<div
|
||||
|
@ -59,6 +65,22 @@ class BoardCard extends React.Component<BoardCardProps, BoardCardState> {
|
|||
this.setState({isDragged: false})
|
||||
this.props.onDragEnd(e)
|
||||
}}
|
||||
|
||||
onDragOver={(e) => {
|
||||
this.setState({isDragOver: true})
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
this.setState({isDragOver: true})
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
this.setState({isDragOver: false})
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
this.setState({isDragOver: false})
|
||||
if (this.props.isDropZone) {
|
||||
this.props.onDrop(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuWrapper
|
||||
className='optionsMenu'
|
||||
|
|
|
@ -4,10 +4,13 @@ import React from 'react'
|
|||
|
||||
type Props = {
|
||||
onDrop?: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
isDropZone?: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
isDragOver?: boolean
|
||||
dragPageX?: number
|
||||
dragPageY?: number
|
||||
}
|
||||
|
||||
class BoardColumn extends React.Component<Props, State> {
|
||||
|
@ -17,24 +20,30 @@ class BoardColumn extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
let className = 'octo-board-column'
|
||||
if (this.props.isDropZone && this.state.isDragOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
const element =
|
||||
(<div
|
||||
className={this.state.isDragOver ? 'octo-board-column dragover' : 'octo-board-column'}
|
||||
className={className}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
this.setState({isDragOver: true})
|
||||
this.setState({isDragOver: true, dragPageX: e.pageX, dragPageY: e.pageY})
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault()
|
||||
this.setState({isDragOver: true})
|
||||
this.setState({isDragOver: true, dragPageX: e.pageX, dragPageY: e.pageY})
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault()
|
||||
this.setState({isDragOver: false})
|
||||
this.setState({isDragOver: false, dragPageX: undefined, dragPageY: undefined})
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
this.setState({isDragOver: false})
|
||||
this.props.onDrop(e)
|
||||
this.setState({isDragOver: false, dragPageX: undefined, dragPageY: undefined})
|
||||
if (this.props.isDropZone) {
|
||||
this.props.onDrop(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
|
|
|
@ -109,6 +109,7 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
|
||||
const {board, activeView, visibleGroups, hiddenGroups} = boardTree
|
||||
const visiblePropertyTemplates = board.cardProperties.filter((template) => activeView.visiblePropertyIds.includes(template.id))
|
||||
const isManualSort = activeView.sortOptions.length < 1
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -182,6 +183,7 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
{visibleGroups.map((group) => (
|
||||
<BoardColumn
|
||||
key={group.option.id || 'empty'}
|
||||
isDropZone={!isManualSort || group.cards.length < 1}
|
||||
onDrop={() => this.onDropToColumn(group.option)}
|
||||
>
|
||||
{group.cards.map((card) => this.renderCard(card, visiblePropertyTemplates))}
|
||||
|
@ -212,6 +214,8 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private renderCard(card: Card, visiblePropertyTemplates: IPropertyTemplate[]) {
|
||||
const {activeView} = this.props.boardTree
|
||||
const isManualSort = activeView.sortOptions.length < 1
|
||||
return (
|
||||
<BoardCard
|
||||
card={card}
|
||||
|
@ -227,6 +231,11 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
onDragEnd={() => {
|
||||
this.draggedCards = []
|
||||
}}
|
||||
|
||||
isDropZone={isManualSort}
|
||||
onDrop={() => {
|
||||
this.onDropToCard(card)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -543,6 +552,39 @@ class BoardComponent extends React.Component<Props, State> {
|
|||
await mutator.changeViewVisibleOptionIds(activeView, visibleOptionIds)
|
||||
}
|
||||
}
|
||||
|
||||
private async onDropToCard(card: Card) {
|
||||
Utils.log(`onDropToCard: ${card.title}`)
|
||||
const {boardTree} = this.props
|
||||
const {activeView} = boardTree
|
||||
const {draggedCards, draggedHeaderOption} = this
|
||||
const optionId = card.properties[activeView.groupById]
|
||||
|
||||
if (draggedCards.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const cardOrder = boardTree.currentCardOrder()
|
||||
for (const draggedCard of draggedCards) {
|
||||
if (draggedCard.id === card.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
Utils.log(`ondrop. Card: ${draggedCard.title}, column: ${optionId}`)
|
||||
const oldValue = draggedCard.properties[boardTree.groupByProperty.id]
|
||||
if (optionId !== oldValue) {
|
||||
await mutator.changePropertyValue(draggedCard, boardTree.groupByProperty.id, optionId, 'drag card')
|
||||
}
|
||||
|
||||
// Change sort position of card
|
||||
const srcIndex = cardOrder.indexOf(draggedCard.id)
|
||||
cardOrder.splice(srcIndex, 1)
|
||||
const destIndex = cardOrder.indexOf(card.id)
|
||||
cardOrder.splice(destIndex, 0, draggedCard.id)
|
||||
}
|
||||
|
||||
await mutator.changeViewCardOrder(activeView, cardOrder)
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(BoardComponent)
|
||||
|
|
|
@ -10,7 +10,7 @@ import Menu from '../widgets/menu'
|
|||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
|
||||
import './comment.scss'
|
||||
import { Utils } from '../utils'
|
||||
import {Utils} from '../utils'
|
||||
|
||||
type Props = {
|
||||
comment: IBlock
|
||||
|
|
|
@ -11,7 +11,7 @@ import mutator from '../mutator'
|
|||
import Comment from './comment'
|
||||
|
||||
import './commentsList.scss'
|
||||
import { MarkdownEditor } from './markdownEditor'
|
||||
import {MarkdownEditor} from './markdownEditor'
|
||||
|
||||
type Props = {
|
||||
comments: readonly IBlock[]
|
||||
|
|
|
@ -4,7 +4,7 @@ import React from 'react'
|
|||
import {injectIntl, IntlShape, FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Archiver} from '../archiver'
|
||||
import {ISortOption} from '../blocks/boardView'
|
||||
import {ISortOption, MutableBoardView} from '../blocks/boardView'
|
||||
import {BlockIcons} from '../blockIcons'
|
||||
import {MutableCard} from '../blocks/card'
|
||||
import {IPropertyTemplate} from '../blocks/board'
|
||||
|
@ -200,7 +200,7 @@ class ViewHeader extends React.Component<Props, State> {
|
|||
key={option.id}
|
||||
id={option.id}
|
||||
name={option.name}
|
||||
icon={boardTree.activeView.groupById === option.id ? <CheckIcon /> : undefined}
|
||||
icon={boardTree.activeView.groupById === option.id ? <CheckIcon/> : undefined}
|
||||
onClick={(id) => {
|
||||
if (boardTree.activeView.groupById === id) {
|
||||
return
|
||||
|
@ -235,15 +235,32 @@ class ViewHeader extends React.Component<Props, State> {
|
|||
/>
|
||||
</div>
|
||||
<Menu>
|
||||
<Menu.Text
|
||||
id='none'
|
||||
name='None'
|
||||
onClick={() => {
|
||||
mutator.changeViewSortOptions(activeView, [])
|
||||
}}
|
||||
/>
|
||||
{(activeView.sortOptions.length > 0) &&
|
||||
<>
|
||||
<Menu.Text
|
||||
id='manual'
|
||||
name='Manual'
|
||||
onClick={() => {
|
||||
// This sets the manual card order to the currently displayed order
|
||||
// Note: Perform this as a single update to change both properties correctly
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.cardOrder = boardTree.currentCardOrder()
|
||||
newView.sortOptions = []
|
||||
mutator.updateBlock(newView, activeView, 'reorder')
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Separator/>
|
||||
<Menu.Text
|
||||
id='revert'
|
||||
name='Revert'
|
||||
onClick={() => {
|
||||
mutator.changeViewSortOptions(activeView, [])
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu.Separator/>
|
||||
</>
|
||||
}
|
||||
|
||||
{boardTree.board.cardProperties.map((option: IPropertyTemplate) => (
|
||||
<Menu.Text
|
||||
|
|
|
@ -17,7 +17,7 @@ import {Utils} from './utils'
|
|||
// It also ensures that the Undo-manager is called for each action
|
||||
//
|
||||
class Mutator {
|
||||
private async updateBlock(newBlock: IBlock, oldBlock: IBlock, description: string): Promise<void> {
|
||||
async updateBlock(newBlock: IBlock, oldBlock: IBlock, description: string): Promise<void> {
|
||||
await undoManager.perform(
|
||||
async () => {
|
||||
await octoClient.updateBlock(newBlock)
|
||||
|
@ -420,6 +420,14 @@ class Mutator {
|
|||
await this.updateBlock(newView, view, 'show column')
|
||||
}
|
||||
|
||||
async changeViewCardOrder(view: BoardView, cardOrder: string[], description = 'reorder'): Promise<void> {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.cardOrder = cardOrder
|
||||
await this.updateBlock(newView, view, description)
|
||||
}
|
||||
|
||||
// Other methods
|
||||
|
||||
// Not a mutator, but convenient to put here since Mutator wraps OctoClient
|
||||
async exportFullArchive(): Promise<IBlock[]> {
|
||||
return octoClient.exportFullArchive()
|
||||
|
|
|
@ -27,6 +27,7 @@ interface BoardTree {
|
|||
readonly groupByProperty?: IPropertyTemplate
|
||||
|
||||
getSearchText(): string | undefined
|
||||
currentCardOrder(): string[]
|
||||
}
|
||||
|
||||
class MutableBoardTree implements BoardTree {
|
||||
|
@ -226,6 +227,22 @@ class MutableBoardTree implements BoardTree {
|
|||
return CardFilter.applyFilterGroup(filterGroup, board.cardProperties, cards)
|
||||
}
|
||||
|
||||
private defaultOrder(cardA: Card, cardB: Card) {
|
||||
const {activeView} = this
|
||||
|
||||
const indexA = activeView.cardOrder.indexOf(cardA.id)
|
||||
const indexB = activeView.cardOrder.indexOf(cardB.id)
|
||||
|
||||
if (indexA < 0 && indexB < 0) {
|
||||
// If both cards' order is not defined, use the create date
|
||||
return cardA.createAt - cardB.createAt
|
||||
} else if (indexA < 0 && indexB >= 0) {
|
||||
// If cardA's order is not defined, put it at the end
|
||||
return 1
|
||||
}
|
||||
return indexA - indexB
|
||||
}
|
||||
|
||||
private sortCards(cards: Card[]): Card[] {
|
||||
if (!this.activeView) {
|
||||
Utils.assertFailure()
|
||||
|
@ -248,11 +265,8 @@ class MutableBoardTree implements BoardTree {
|
|||
if (bValue && !aValue) {
|
||||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
}
|
||||
|
||||
return a.createAt - b.createAt
|
||||
return this.defaultOrder(a, b)
|
||||
})
|
||||
} else {
|
||||
sortOptions.forEach((sortOption) => {
|
||||
|
@ -270,7 +284,7 @@ class MutableBoardTree implements BoardTree {
|
|||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
return this.defaultOrder(a, b)
|
||||
}
|
||||
|
||||
let result = aValue.localeCompare(bValue)
|
||||
|
@ -296,7 +310,7 @@ class MutableBoardTree implements BoardTree {
|
|||
return 1
|
||||
}
|
||||
if (!a.title && !b.title) {
|
||||
return a.createAt - b.createAt
|
||||
return this.defaultOrder(a, b)
|
||||
}
|
||||
|
||||
const aValue = a.properties[sortPropertyId] || ''
|
||||
|
@ -311,7 +325,7 @@ class MutableBoardTree implements BoardTree {
|
|||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
return this.defaultOrder(a, b)
|
||||
}
|
||||
|
||||
// Sort by the option order (not alphabetically by value)
|
||||
|
@ -328,12 +342,12 @@ class MutableBoardTree implements BoardTree {
|
|||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
return this.defaultOrder(a, b)
|
||||
}
|
||||
|
||||
result = Number(aValue) - Number(bValue)
|
||||
} else if (template.type === 'createdTime') {
|
||||
result = a.createAt - b.createAt
|
||||
result = this.defaultOrder(a, b)
|
||||
} else if (template.type === 'updatedTime') {
|
||||
result = a.updateAt - b.updateAt
|
||||
} else {
|
||||
|
@ -347,7 +361,7 @@ class MutableBoardTree implements BoardTree {
|
|||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
return this.defaultOrder(a, b)
|
||||
}
|
||||
|
||||
result = aValue.localeCompare(bValue)
|
||||
|
@ -364,6 +378,18 @@ class MutableBoardTree implements BoardTree {
|
|||
|
||||
return sortedCards
|
||||
}
|
||||
|
||||
currentCardOrder(): string[] {
|
||||
const cardOrder: string[] = []
|
||||
for (const group of this.visibleGroups) {
|
||||
cardOrder.push(...group.cards.map((o) => o.id))
|
||||
}
|
||||
for (const group of this.hiddenGroups) {
|
||||
cardOrder.push(...group.cards.map((o) => o.id))
|
||||
}
|
||||
|
||||
return cardOrder
|
||||
}
|
||||
}
|
||||
|
||||
export {MutableBoardTree, BoardTree, Group as BoardTreeGroup}
|
||||
|
|
Loading…
Reference in a new issue