Manual card order

This commit is contained in:
Chen-I Lim 2020-10-28 10:46:36 -07:00
parent f5667304f6
commit 8794145802
9 changed files with 163 additions and 30 deletions

View file

@ -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'

View file

@ -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'

View file

@ -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}

View file

@ -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)

View file

@ -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

View file

@ -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[]

View file

@ -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

View file

@ -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()

View file

@ -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}