@bigfishtv/cockpit
Version:
394 lines (365 loc) • 12.1 kB
JavaScript
import React, { Component } from 'react'
import { Fieldset } from '@bigfishtv/react-forms'
import deepEqual from 'deep-equal'
import classnames from 'classnames'
import _get from 'lodash/get'
import _truncate from 'lodash/truncate'
import * as xhrUtils from '../../api/xhrUtils'
import { showDeletePrompt } from '../../utils/promptUtils'
import { titleCase } from '../../utils/stringUtils'
import { modalHandler } from '../modal/ModalHost'
import {
flatten,
getParentByChildId,
sortByKey,
pruneTree,
appendChildToParent,
replaceChild,
} from '../../utils/treeUtils'
import modalFormValueContext from '../../decorators/modalFormValueContext'
import { AutoTableIndexContainer, AutoTableIndexBase } from './AutoTableIndex'
import MainContent from '../container/MainContent'
import Bulkhead from '../page/Bulkhead'
import Panel from '../container/panel/Panel'
import Tree from '../tree/Tree'
import Field from '../form/Field'
import Modal from '../modal/Modal'
import TreeSelectModal from '../modal/TreeSelectModal'
import Button from '../button/Button'
import DropdownItem from '../button/dropdown/DropdownItem'
import DropdownItemDivider from '../button/dropdown/DropdownItemDivider'
import DropdownButton from '../button/dropdown/DropdownButton'
import Icon from '../Icon'
import TreeModal from '../modal/TreeModal'
const DefaultBulkheadToolbar = ({ modelLabel, model, addUrl }) => (
<Button
text={'New ' + titleCase(modelLabel || model)}
onClick={() => (window.location.href = addUrl)}
style="primary"
size="large"
/>
)
// used in sidebar
const DefaultTreeCell = props => {
const {
id,
title,
locked,
isCollapsed,
showIndicator,
onIndicatorClick,
onIndicatorDoubleClick,
isOver,
position,
onClick,
onDoubleClick,
selected,
data,
} = props
const items =
id === null ? data : data.filter(({ folder_id }) => (id === '_unsorted' ? folder_id === null : folder_id === id))
return (
<div className={classnames('tree-item', isOver && 'drag-' + position)}>
<div
className={classnames('tree-cell tree-cell-small', { selected: selected })}
onClick={onClick}
onDoubleClick={onDoubleClick}>
{showIndicator ? (
<div
className={classnames('tree-cell-icon', isCollapsed && 'collapsed')}
onClick={onIndicatorClick}
onDoubleClick={onIndicatorDoubleClick}>
<Icon name={'chevron-' + (isCollapsed ? 'right' : 'down')} size={18} />
</div>
) : (
<div className="tree-cell-icon" />
)}
{locked && (
<div className="tree-cell-icon">
<Icon name="lock" size={12} />
</div>
)}
<div className="tree-cell-title">
{_truncate(title, { length: 45 })} ({items.length})
</div>
</div>
</div>
)
}
// used in sidebar
const DefaultTray = props => {
const { onFolderAdd, onFolderDelete, onEdit, currentFolderId } = props
return (
<div className="finder-menu-footer">
<Button size="small" text="Add Folder" onClick={onFolderAdd} />
<Button size="small" text="Delete" onClick={onFolderDelete} />
<Button size="small" text="Edit" onClick={onEdit} disabled={currentFolderId === null} />
</div>
)
}
export const DefaultFolderSidebarToolbar = props => (
<DropdownButton style="icon" caret={false} pullRight={true} text={<Icon name="hamburger" />}>
<DropdownItem text="New Folder" icon="folder-new" onClick={props.handleFolderAdd} />
<DropdownItem text="Edit" icon="edit" onClick={props.handleFolderEdit} disabled={!props.currentFolderId} />
<DropdownItemDivider />
<DropdownItem text="Remove" icon="remove" onClick={props.handleFolderDelete} disabled={!props.currentFolderId} />
</DropdownButton>
)
export class DefaultFolderEditModal extends Component {
static defaultProps = {
isNew: false,
}
handleSave = () => {
this.props.onSave(this.props.formValue, this.props.isNew)
}
handleClose = (didSave = false) => {
this.props.onClose(this.props.formValue, didSave, this.props.isNew)
}
render() {
const { foldersUrl } = this.props
const { title } = this.props.formValue.value
return (
<Modal
{...this.props}
title={title || 'New Folder'}
size="small"
onClose={this.handleClose}
onSave={this.handleSave}>
<Fieldset formValue={this.props.formValue}>
<Field select="title" label="Title" placeholder="Title" autoFocus />
<Field select="parent" label="Parent Folder">
<TreeSelectModal
buttonText="Select Folder"
modalProps={{
url: foldersUrl,
title: 'Select Parent Folder',
primaryActionText: 'Select Parent Folder',
filterData: (data, user) => sortByKey(data, 'title'),
}}
/>
</Field>
</Fieldset>
</Modal>
)
}
}
export default class AutoFolderTableIndex extends Component {
static defaultProps = {
Tray: DefaultTray,
TreeCell: DefaultTreeCell,
BulkheadToolbar: DefaultBulkheadToolbar,
SidebarToolbar: DefaultFolderSidebarToolbar,
FolderEditModal: DefaultFolderEditModal,
foldersUrl: '/tank/folders.json',
folderDeleteUrl: '/tank/folders/delete.json',
getFolderSubmitUrl: data => `/tank/folders/${data.id ? 'edit/' + data.id : 'add'}.json`,
componentResolver: () => {},
attributeModified: () => {},
newFolderDefaultValue: {},
}
constructor(props) {
super(props)
this.state = {
foldersData: props.folders || [],
currentFolderId: null,
collapsedFolderIds: [],
}
}
handleFolderSelection = (ids, handleFilterChange) => {
const currentFolderId = ids[0]
this.setState(
{ currentFolderId },
() =>
handleFilterChange &&
handleFilterChange('folder_id', currentFolderId == '_unsorted' ? val => val === null : currentFolderId)
)
}
handleFolderCollapse = collapsedIds => {
this.setState({ collapsedIds })
}
handleFolderAdd = () => {
console.log('opening folder add modal')
modalHandler.add({
Component: this.props.FolderEditModal,
props: {
isNew: true,
defaultValue: this.props.newFolderDefaultValue,
foldersUrl: this.props.foldersUrl,
onSave: this.handleFolderEditSave.bind(this, {}),
onClose: this.handleFolderEditClose.bind(this, {}),
},
})
}
handleFolderEdit = () => {
const { foldersData, currentFolderId } = this.state
if (!currentFolderId) return
// because folder data is threaded we gotta flatten it to find current selection and set its parent
const folder = flatten(foldersData).filter(item => item.id === currentFolderId)[0]
folder.parent = getParentByChildId(currentFolderId, foldersData)
modalHandler.add({
Component: this.props.FolderEditModal,
props: {
defaultValue: folder,
foldersUrl: this.props.foldersUrl,
onSave: this.handleFolderEditSave.bind(this, folder),
onClose: this.handleFolderEditClose.bind(this, folder),
},
})
}
handleFolderEditSave = (oldRowValue, newRowValue, isNew) => {
newRowValue = newRowValue.value
if (!deepEqual(oldRowValue, newRowValue)) {
const postUrl = this.props.getFolderSubmitUrl({ id: isNew ? false : newRowValue.id })
const hasNewParent = _get(newRowValue, 'parent.id', null) != _get(oldRowValue, 'parent.id')
if (hasNewParent) newRowValue.parent_id = _get(newRowValue, 'parent.id', null)
xhrUtils.post({
url: postUrl,
data: newRowValue,
callback: folder => {
let foldersData = this.state.foldersData
if (isNew) {
foldersData = appendChildToParent(foldersData, folder.parent_id, folder)
foldersData = sortByKey(foldersData, 'title')
} else {
if (hasNewParent) {
foldersData = pruneTree(foldersData, 'id', folder.id)
foldersData = appendChildToParent(foldersData, folder.parent_id, folder)
foldersData = sortByKey(foldersData, 'title')
} else {
foldersData = replaceChild(this.state.foldersData, folder)
}
}
this.setState({ foldersData })
this.handleFolderSelection([folder.id])
},
})
} else {
console.log('Nothing edited, not saving.')
}
}
handleFolderEditClose = (oldRowValue, newFormValue, isNew) => {
console.log('MODAL CLOSED, W/E')
}
handleFolderDelete = () => {
const { foldersData, currentFolderId } = this.state
showDeletePrompt({
subject: 'email template folder',
queryUrl: this.props.folderDeleteUrl,
style: 'error',
selectedIds: [currentFolderId],
data: flatten(foldersData).filter(item => item.id === currentFolderId),
callback: response => {
this.setState({
foldersData: pruneTree(foldersData, 'id', [currentFolderId]),
currentFolderId: null,
})
},
})
}
handleMove = (selectedItems, updateCallback) => {
modalHandler.add({
Component: TreeModal,
props: {
title: 'Select Folder',
primaryActionText: 'Confirm',
url: this.props.foldersUrl,
onClose: () => {},
onSave: folder => {
if (folder) {
const selectedIds = selectedItems.map(({ id }) => id)
xhrUtils.post({
url: `${this.props.moveUrl}?folder_id=${folder.id}`,
data: { id: selectedIds },
successMessage: `${selectedItems.length} item${selectedItems.length > 1 ? 's' : ''} moved to ${
folder.title
}`,
failureMessage: 'There was an error moving items',
callback: () => {
const updateDataFunc = data =>
data.map(item => (~selectedIds.indexOf(item.id) ? { ...item, folder_id: folder.id } : item))
updateCallback(updateDataFunc)
},
})
}
},
},
})
}
render() {
const { SidebarToolbar, BulkheadToolbar, TreeCell, sidebarTitle } = this.props
const { collapsedFolderIds, currentFolderId, foldersData } = this.state
const sidebarProps = {
currentFolderId,
handleFolderAdd: this.handleFolderAdd,
handleFolderEdit: this.handleFolderEdit,
handleFolderDelete: this.handleFolderDelete,
}
const _foldersData = [{ id: null, title: '[All]' }, { id: '_unsorted', title: '[Unsorted]' }, ...foldersData]
return (
<AutoTableIndexContainer
{...this.props}
defaultValue={this.props.data}
movable={!!this.props.moveUrl}
onMove={this.handleMove}>
{props => {
const { model, panelProps, handleFilterChange, originalData } = props
return (
<MainContent>
<Bulkhead title={titleCase(model)} Toolbar={() => <BulkheadToolbar {...props} {...panelProps} />} />
<div className="finder">
<div className="finder-menu">
<Panel title={sidebarTitle || 'Folders'} PanelToolbar={() => <SidebarToolbar {...sidebarProps} />}>
<Tree
value={_foldersData}
TreeCell={props => <TreeCell {...props} data={originalData} />}
dropTargetType={'TANK_FOLDER'}
treeItemTarget={treeItemTarget(this)}
treeItemSource={treeItemSource(this)}
onSelectItem={this.handleFolderEdit}
selectedIds={[currentFolderId]}
collapsedIds={collapsedFolderIds}
onSelectionChange={ids => this.handleFolderSelection(ids, handleFilterChange)}
onCollapseChange={this.handleFolderCollapse}
onCombinationChange={() => console.log('HANDLE COMBINATION CHANGE')}
/>
</Panel>
</div>
<div className="finder-content" ref="finderContent">
<AutoTableIndexBase {...props} />
</div>
</div>
</MainContent>
)
}}
</AutoTableIndexContainer>
)
}
}
// drag source & target configs for allowing projects to be dropped into sector folders
const treeItemSource = parent => ({
beginDrag(props, monitor, component) {
return { id: props.id }
},
})
const treeItemTarget = parent => ({
drop(props, monitor, component) {
if (monitor.isOver({ shallow: true })) {
if (!props.selected) {
const sectorId = monitor.getItem().id
const parentId = props.id
parent.onMoveSector(parentId, sectorId)
} else {
console.log('no need to move asset into already opened folder')
}
}
},
hover(props, monitor, component) {
if (monitor.isOver({ shallow: true })) {
const ownId = props.id
const draggedId = monitor.getItem().id
if (draggedId === ownId) return
component.setState({ position: 'into' })
}
},
})