Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow cards to be dragged from page to page #545

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions app/controllers/cards_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ def create
def update
authorize @card

if @card.update(card_params)
render @card
form = UpdateCardForm.new card: @card
form.assign_attributes card_params

if form.save
render form.card
else
render json: @card.errors, status: :unprocessable_entity
render json: form.errors, status: :unprocessable_entity
end
end

Expand All @@ -57,7 +60,8 @@ def card_params
Sv.hash_of(
position: Sv.scalar,
solid: Sv.scalar,
raw_content: raw_draft_content_state
raw_content: raw_draft_content_state,
page_id: Sv.scalar
).(params.require(:card))
end

Expand Down
6 changes: 3 additions & 3 deletions app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ class PagesController < ApplicationController
# @route [POST] `/cases/case-slug/pages`
def create
@page = Page.new page_params
@page.build_case_element case: @case
@page.build_case_element case: @case, position: params[:page][:position]

authorize @page

if @page.save
render @page
render json: @page
else
render json: @page.errors, status: :unprocessable_entity
end
Expand Down Expand Up @@ -55,6 +55,6 @@ def set_page
end

def page_params
params[:page].permit(:title, :position, :icon_slug)
params[:page].permit(:title, :icon_slug)
end
end
25 changes: 25 additions & 0 deletions app/forms/move_card_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

# For validating updates to {Card}s
class UpdateCardForm
include ActiveModel::Model

attr_accessor :card

validate :new_element_is_page_in_case?

def save
return unless valid? && card.valid?

card.save
end

private

def new_element_is_page_in_case?
return unless card.element_changed?
return unless card.element.is_a?(Page) && card.element.case == card.case

errors.add :card, 'can only be moved to a Page in the same Case'
end
end
44 changes: 44 additions & 0 deletions app/forms/update_card_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

# For validating updates to {Card}s
class UpdateCardForm
include ActiveModel::Model

attr_accessor :card, :position, :solid, :raw_content, :page_id

validates :card, presence: true
validate :new_element_is_page_in_case?

def save
card.assign_attributes card_params

return unless valid?

card.save!
true
end

private

def new_element_is_page_in_case?
return unless page.present?
return if page.case == card.case

errors.add :card, 'can only be moved to a Page in the same Case'
end

def page
@page ||= Page.find page_id
rescue ActiveRecord::RecordNotFound
nil
end

def card_params
{
position: position,
solid: solid,
raw_content: raw_content,
element: page
}.compact
end
end
2 changes: 1 addition & 1 deletion app/javascript/overview/TableOfContents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function TableOfContents ({
</NoElements>
)}

<Droppable droppableId="table-of-contents" type="CaseElement">
<Droppable droppableId="table-of-contents" isCombineEnabled={onSidebar}>
{(provided, snapshot) => (
<List
ref={provided.innerRef}
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/overview/TableOfContentsElement.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function TableOfContentsElement ({
}: Props) {
return (
<Draggable
draggableId={caseElement.id}
draggableId={`caseElements/${caseElement.id}`}
index={position}
isDragDisabled={readOnly || !editing}
>
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/page/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const Page = (props: Props) => {
let { id, title, cards } = page

return (
<Droppable droppableId={`pages/${id}`} type="Page">
<Droppable droppableId={`pages/${id}`}>
{({ placeholder, innerRef: droppableRef }) => (
<div ref={droppableRef}>
<article>
Expand Down
45 changes: 43 additions & 2 deletions app/javascript/redux/actions/card.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* @flow
*/

import { setUnsaved, displayToast } from 'redux/actions'
import { createPage, setUnsaved, displayToast } from 'redux/actions'

import { Orchard } from 'shared/orchard'
import { Intent } from '@blueprintjs/core'
import { EditorState } from 'draft-js'

import type { ThunkAction, Dispatch } from 'redux/actions'
import type { ThunkAction, Dispatch, GetState } from 'redux/actions'
import type { CardsState, Card, Citation } from 'redux/state'

export type SetCardsAction = { type: 'SET_CARDS', cards: CardsState }
Expand Down Expand Up @@ -74,11 +74,52 @@ export function reorderCard (
return { type: 'REORDER_CARD', id, destination }
}

export function moveCardToNewPage (id: string, pageIndex: number): ThunkAction {
return async (dispatch: Dispatch, getState: GetState) => {
const {
caseData: { slug },
} = getState()

dispatch(removeCard(id))
const page = await dispatch(createPage(slug, { position: pageIndex + 1 }))
return dispatch(moveCardToPage(id, page.param))
}
}

export function moveCardToExistingPage (
id: string,
draggableId: string
): ThunkAction {
return (dispatch: Dispatch, getState: GetState) => {
const [, caseElementId] = draggableId.split('/')
const caseElement = getState().caseData.caseElements.find(
e => e.param === caseElementId
)
if (caseElement == null) return

const { elementType, elementId } = caseElement

if (elementType === 'Page') {
dispatch(removeCard(id))
return dispatch(moveCardToPage(id, elementId))
}
}
}

function moveCardToPage (cardId, pageId): ThunkAction {
return async (dispatch: Dispatch) => {
console.log(`moveCardToPage(${cardId}, ${pageId})`)
const card = await Orchard.espalier(`cards/${cardId}`, { card: { pageId }})
return dispatch(addCard(pageId, card))
}
}

export type RemoveCardAction = {
type: 'REMOVE_CARD',
id: string,
}
export function removeCard (id: string): RemoveCardAction {
console.log(`removeCard(${id})`)
return { type: 'REMOVE_CARD', id }
}

Expand Down
12 changes: 8 additions & 4 deletions app/javascript/redux/actions/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ export function addPage (data: Page): AddPageAction {
return { type: 'ADD_PAGE', data }
}

export function createPage (caseSlug: string): ThunkAction {
export function createPage (
caseSlug: string,
data: $Shape<Page> = {}
): ThunkAction {
return async (dispatch: Dispatch) => {
const data: Page = await Orchard.graft(`cases/${caseSlug}/pages`, {})
dispatch(addPage(data))
dispatch(createCard(data.id))
const page: Page = await Orchard.graft(`cases/${caseSlug}/pages`, data)
dispatch(addPage(page))
if (!data.position) dispatch(createCard(page.id))
return page
}
}

Expand Down
25 changes: 25 additions & 0 deletions app/javascript/redux/reducers/__tests__/caseData.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* @noflow */

import reducer from '../caseData.js'

describe('caseData reducer', () => {
describe('AddPageAction', () => {
test('works', () => {
const state = {
caseElements: [{ id: 1, position: 1 }, { id: 2, position: 2 }],
}
const action = {
type: 'ADD_PAGE',
data: { caseElement: { id: 3, position: 2 }},
}

expect(reducer(state, action)).toEqual({
caseElements: [
{ id: 1, position: 1 },
{ id: 3, position: 2 },
{ id: 2, position: 2 },
],
})
})
})
})
7 changes: 6 additions & 1 deletion app/javascript/redux/reducers/caseData.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import update from 'immutability-helper'
import { insert } from 'ramda'

import type {
UpdateCaseAction,
Expand Down Expand Up @@ -70,7 +71,11 @@ export default function caseData (
const { caseElement } = action.data
return {
...state,
caseElements: [...state.caseElements, caseElement],
caseElements: insert(
caseElement.position - 1,
caseElement,
state.caseElements
),
}
}

Expand Down
1 change: 1 addition & 0 deletions app/javascript/redux/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export type CaseElement = {
elementStore: CaseElementStore,
elementType: string,
id: string,
param: string,
position: number,
}

Expand Down
54 changes: 43 additions & 11 deletions app/javascript/shared/GalaDragDropContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,76 @@ import * as React from 'react'
import { connect } from 'react-redux'
import { DragDropContext } from 'react-beautiful-dnd'

import { reorderCaseElements, reorderCard } from 'redux/actions'
import {
moveCardToNewPage,
moveCardToExistingPage,
reorderCaseElements,
reorderCard,
} from 'redux/actions'

import type { State } from 'redux/state'

type Props = {
children: React.Node,
moveCardToExistingPage: typeof moveCardToExistingPage,
moveCardToNewPage: typeof moveCardToNewPage,
reorderCaseElements: typeof reorderCaseElements,
reorderCard: typeof reorderCard,
}

function GalaDragDropContext ({
children,
moveCardToExistingPage,
moveCardToNewPage,
reorderCaseElements,
reorderCard,
}: Props) {
return <DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext>

function onDragEnd (result) {
const { draggableId, type, source, destination } = result
const { draggableId, source, destination } = result
const [table, param] = draggableId.split('/')

switch (type) {
case 'CaseElement': {
switch (table) {
case 'caseElements': {
if (!destination) return

reorderCaseElements(source.index, destination.index)
break
}

case 'Page': {
if (!destination) return

const [, cardId] = draggableId.split('/')
reorderCard(cardId, destination.index)
case 'cards': {
handleCardDragEnd(param, result)
break
}
}
}

return <DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext>
function handleCardDragEnd (cardId, result) {
const { destination } = result

if (destination && destination.droppableId.match(/^pages/)) {
reorderCard(cardId, destination.index)
} else {
moveCard(cardId, result)
}
}

function moveCard (cardId, { destination, combine }) {
if (destination) {
moveCardToNewPage(cardId, destination.index)
} else if (combine) {
moveCardToExistingPage(cardId, combine.draggableId)
}
}
}

export default connect(
null,
{ reorderCaseElements, reorderCard }
{
moveCardToNewPage,
moveCardToExistingPage,
reorderCaseElements,
reorderCard,
}
)(GalaDragDropContext)
4 changes: 4 additions & 0 deletions app/javascript/table_of_contents/shared.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export const List = styled.ol`
padding: 0;
width: 100%;
#Sidebar & {
width: 224px;
}
${p =>
p.isDraggingOver &&
css`
Expand Down
Loading